Comparing version 0.0.0 to 0.0.1
@@ -41,32 +41,28 @@ #!/usr/bin/env node | ||
var graphql_1 = require("graphql"); | ||
var _1 = require("./"); | ||
var lib_1 = require("./lib"); | ||
var glob_1 = require("glob"); | ||
/** | ||
* Build a schema from a glob pattern. | ||
* Extract schema from TypeScript files and print to stdout. | ||
* | ||
* Usage: node dist/cli.js "./**.ts" | ||
* Will search for a tsconfig.json file relative to the current working | ||
* directory. | ||
* | ||
* There are not yet any command line options. | ||
* | ||
* Usage: grats | ||
*/ | ||
function main() { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var pattern, files, schemaResult; | ||
var parsed, schemaResult; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
pattern = process.argv[2]; | ||
if (!pattern) { | ||
throw new Error("Expected glob as first argument"); | ||
} | ||
return [4 /*yield*/, (0, glob_1.glob)(pattern)]; | ||
case 1: | ||
files = _a.sent(); | ||
schemaResult = (0, lib_1.buildSchemaResult)({ files: files }); | ||
if (schemaResult.kind === "ERROR") { | ||
console.error(schemaResult.err.formatDiagnosticsWithColorAndContext()); | ||
process.exit(1); | ||
} | ||
else { | ||
console.log((0, graphql_1.printSchema)(schemaResult.value)); | ||
} | ||
return [2 /*return*/]; | ||
parsed = (0, _1.getParsedTsConfig)(); | ||
schemaResult = (0, lib_1.buildSchemaResult)((0, _1.gratsOptionsFromTsConfig)(parsed)); | ||
if (schemaResult.kind === "ERROR") { | ||
console.error(schemaResult.err.formatDiagnosticsWithColorAndContext()); | ||
process.exit(1); | ||
} | ||
else { | ||
console.log((0, graphql_1.printSchema)(schemaResult.value)); | ||
} | ||
return [2 /*return*/]; | ||
}); | ||
@@ -73,0 +69,0 @@ }); |
@@ -5,3 +5,3 @@ import { DefinitionNode, FieldDefinitionNode, InputValueDefinitionNode, ListTypeNode, NamedTypeNode, Location as GraphQLLocation, NameNode, Token, TypeNode, NonNullTypeNode, StringValueNode, ConstValueNode, ConstDirectiveNode, ConstArgumentNode, EnumValueDefinitionNode } from "graphql"; | ||
import { TypeContext } from "./TypeContext"; | ||
import { BuildOptions } from "./lib"; | ||
import { ConfigOptions } from "./lib"; | ||
type ArgDefaults = Map<string, ts.Expression>; | ||
@@ -22,5 +22,5 @@ /** | ||
ctx: TypeContext; | ||
buildOptions: BuildOptions; | ||
configOptions: ConfigOptions; | ||
errors: ts.Diagnostic[]; | ||
constructor(sourceFile: ts.SourceFile, ctx: TypeContext, buildOptions: BuildOptions); | ||
constructor(sourceFile: ts.SourceFile, ctx: TypeContext, buildOptions: ConfigOptions); | ||
extract(): DiagnosticsResult<DefinitionNode[]>; | ||
@@ -33,5 +33,6 @@ extractType(node: ts.Node, tag: ts.JSDocTag): void; | ||
extractUnion(node: ts.Node, tag: ts.JSDocTag): void; | ||
extractExtendType(node: ts.Node, tag: ts.JSDocTag): void; | ||
/** Error handling and location juggling */ | ||
report(node: ts.Node, message: string): void; | ||
reportUnhandled(node: ts.Node, message: string): void; | ||
report(node: ts.Node, message: string): null; | ||
reportUnhandled(node: ts.Node, message: string): null; | ||
diagnosticAnnotatedLocation(node: ts.Node): { | ||
@@ -46,2 +47,5 @@ start: number; | ||
unionTypeAliasDeclaration(node: ts.TypeAliasDeclaration, tag: ts.JSDocTag): null | undefined; | ||
functionDeclarationExtendType(node: ts.FunctionDeclaration, tag: ts.JSDocTag): null | undefined; | ||
typeReferenceFromParam(typeParam: ts.ParameterDeclaration): NameNode | null; | ||
namedFunctionExportName(node: ts.FunctionDeclaration): ts.Identifier | null; | ||
scalarTypeAliasDeclaration(node: ts.TypeAliasDeclaration, tag: ts.JSDocTag): null | undefined; | ||
@@ -52,6 +56,7 @@ inputTypeAliasDeclaration(node: ts.TypeAliasDeclaration, tag: ts.JSDocTag): null | undefined; | ||
typeClassDeclaration(node: ts.ClassDeclaration, tag: ts.JSDocTag): null | undefined; | ||
collectInterfaces(node: ts.ClassDeclaration): Array<NamedTypeNode> | null; | ||
typeInterfaceDeclaration(node: ts.InterfaceDeclaration, tag: ts.JSDocTag): null | undefined; | ||
collectInterfaces(node: ts.ClassDeclaration | ts.InterfaceDeclaration): Array<NamedTypeNode> | null; | ||
interfaceInterfaceDeclaration(node: ts.InterfaceDeclaration, tag: ts.JSDocTag): void; | ||
collectFields(node: ts.ClassDeclaration | ts.InterfaceDeclaration): Array<FieldDefinitionNode>; | ||
collectArgs(node: ts.MethodDeclaration): ReadonlyArray<InputValueDefinitionNode> | null; | ||
collectArgs(argsParam: ts.ParameterDeclaration): ReadonlyArray<InputValueDefinitionNode> | null; | ||
collectArgDefaults(node: ts.ObjectBindingPattern): ArgDefaults; | ||
@@ -63,3 +68,3 @@ collectConstValue(node: ts.Expression): ConstValueNode | null; | ||
collectEnumValues(node: ts.EnumDeclaration): ReadonlyArray<EnumValueDefinitionNode>; | ||
entityName(node: ts.ClassDeclaration | ts.MethodDeclaration | ts.PropertyDeclaration | ts.InterfaceDeclaration | ts.PropertySignature | ts.EnumDeclaration | ts.TypeAliasDeclaration, tag: ts.JSDocTag): NameNode | null; | ||
entityName(node: ts.ClassDeclaration | ts.MethodDeclaration | ts.PropertyDeclaration | ts.InterfaceDeclaration | ts.PropertySignature | ts.EnumDeclaration | ts.TypeAliasDeclaration | ts.FunctionDeclaration, tag: ts.JSDocTag): NameNode | null; | ||
methodDeclaration(node: ts.MethodDeclaration): FieldDefinitionNode | null; | ||
@@ -76,2 +81,3 @@ collectDescription(node: ts.Node): StringValueNode | null; | ||
methodNameDirective(nameNode: ts.Node, name: string): ConstDirectiveNode; | ||
exportDirective(nameNode: ts.Node, filename: string, functionName: string): ConstDirectiveNode; | ||
/** GraphQL AST node helper methods */ | ||
@@ -78,0 +84,0 @@ gqlName(node: ts.Node, value: string): NameNode; |
@@ -20,5 +20,6 @@ "use strict"; | ||
var serverDirectives_1 = require("./serverDirectives"); | ||
var LIBRARY_IMPORT_NAME = "<library import name>"; | ||
var LIBRARY_NAME = "<library name>"; | ||
var ISSUE_URL = "<issue URL>"; | ||
var path_1 = require("path"); | ||
var LIBRARY_IMPORT_NAME = "grats"; | ||
var LIBRARY_NAME = "Grats"; | ||
var ISSUE_URL = "https://github.com/captbaritone/grats/issues"; | ||
var TYPE_TAG = "GQLType"; | ||
@@ -31,2 +32,3 @@ var FIELD_TAG = "GQLField"; | ||
var INPUT_TAG = "GQLInput"; | ||
var EXTEND_TYPE = "GQLExtendType"; | ||
var DEPRECATED_TAG = "deprecated"; | ||
@@ -49,3 +51,3 @@ /** | ||
this.ctx = ctx; | ||
this.buildOptions = buildOptions; | ||
this.configOptions = buildOptions; | ||
} | ||
@@ -88,2 +90,5 @@ // Traverse all nodes, checking each one for its JSDoc tags. | ||
break; | ||
case EXTEND_TYPE: | ||
_this.extractExtendType(node, tag); | ||
break; | ||
} | ||
@@ -109,4 +114,7 @@ } | ||
} | ||
else if (ts.isInterfaceDeclaration(node)) { | ||
this.typeInterfaceDeclaration(node, tag); | ||
} | ||
else { | ||
this.report(tag, "`@".concat(TYPE_TAG, "` can only be used on class declarations.")); | ||
this.report(tag, "`@".concat(TYPE_TAG, "` can only be used on class or interface declarations.")); | ||
} | ||
@@ -157,2 +165,11 @@ }; | ||
}; | ||
Extractor.prototype.extractExtendType = function (node, tag) { | ||
if (ts.isFunctionDeclaration(node)) { | ||
// FIXME: Validate that the function is a named export | ||
this.functionDeclarationExtendType(node, tag); | ||
} | ||
else { | ||
this.report(tag, "`@".concat(EXTEND_TYPE, "` can only be used on function declarations.")); | ||
} | ||
}; | ||
/** Error handling and location juggling */ | ||
@@ -170,2 +187,3 @@ Extractor.prototype.report = function (node, message) { | ||
}); | ||
return null; | ||
}; | ||
@@ -178,2 +196,3 @@ // Report an error that we don't know how to infer a type, but it's possible that we should. | ||
this.report(node, completedMessage); | ||
return null; | ||
}; | ||
@@ -205,4 +224,3 @@ Extractor.prototype.diagnosticAnnotatedLocation = function (node) { | ||
if (!ts.isUnionTypeNode(node.type)) { | ||
this.report(node, "Expected a TypeScript union. `@".concat(UNION_TAG, "` can only be used on TypeScript unions.")); | ||
return null; | ||
return this.report(node, "Expected a TypeScript union. `@".concat(UNION_TAG, "` can only be used on TypeScript unions.")); | ||
} | ||
@@ -215,7 +233,6 @@ var description = this.collectDescription(node.name); | ||
if (!ts.isTypeReferenceNode(member)) { | ||
this.reportUnhandled(member, "Expected `@".concat(UNION_TAG, "` union members to be type references.")); | ||
return null; | ||
return this.reportUnhandled(member, "Expected `@".concat(UNION_TAG, "` union members to be type references.")); | ||
} | ||
var namedType = this.gqlNamedType(member.typeName, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
this.ctx.markUnresolvedType(member.typeName, namedType); | ||
this.ctx.markUnresolvedType(member.typeName, namedType.name); | ||
types.push(namedType); | ||
@@ -240,2 +257,90 @@ } | ||
}; | ||
Extractor.prototype.functionDeclarationExtendType = function (node, tag) { | ||
var funcName = this.namedFunctionExportName(node); | ||
if (funcName == null) | ||
return null; | ||
var typeParam = node.parameters[0]; | ||
if (typeParam == null) { | ||
return this.report(funcName, "Expected type extension function to have a first argument representing the type to extend."); | ||
} | ||
var typeName = this.typeReferenceFromParam(typeParam); | ||
if (typeName == null) | ||
return null; | ||
var name = this.entityName(node, tag); | ||
if (name == null) | ||
return null; | ||
if (node.type == null) { | ||
return this.report(funcName, "Expected GraphQL field to have an explicit return type."); | ||
} | ||
var type = this.collectType(node.type); | ||
if (type == null) | ||
return null; | ||
var args = null; | ||
var argsParam = node.parameters[1]; | ||
if (argsParam != null) { | ||
args = this.collectArgs(argsParam); | ||
} | ||
var description = this.collectDescription(funcName); | ||
if (!ts.isSourceFile(node.parent)) { | ||
return this.report(node, "Expected type extension function to be a top-level declaration."); | ||
} | ||
// TODO: Does this work in the browser? | ||
var filename = (0, path_1.relative)(this.ctx.host.getCurrentDirectory(), node.parent.fileName); | ||
var directives = [this.exportDirective(funcName, filename, funcName.text)]; | ||
if (funcName.text !== name.value) { | ||
directives = [this.methodNameDirective(funcName, funcName.text)]; | ||
} | ||
var deprecated = this.collectDeprecated(node); | ||
if (deprecated != null) { | ||
directives.push(deprecated); | ||
} | ||
this.definitions.push({ | ||
kind: graphql_1.Kind.OBJECT_TYPE_EXTENSION, | ||
loc: this.loc(node), | ||
name: typeName, | ||
fields: [ | ||
{ | ||
kind: graphql_1.Kind.FIELD_DEFINITION, | ||
loc: this.loc(node), | ||
description: description || undefined, | ||
name: name, | ||
arguments: args || undefined, | ||
type: this.handleErrorBubbling(type), | ||
directives: directives.length === 0 ? undefined : directives | ||
}, | ||
] | ||
}); | ||
}; | ||
Extractor.prototype.typeReferenceFromParam = function (typeParam) { | ||
if (typeParam.type == null) { | ||
return this.report(typeParam, "Expected first argument of a type extension function to have an explicit type annotation."); | ||
} | ||
if (!ts.isTypeReferenceNode(typeParam.type)) { | ||
return this.report(typeParam.type, "Expected first argument of a type extension function to be typed as a `@GQLType` type."); | ||
} | ||
var nameNode = typeParam.type.typeName; | ||
var typeName = this.gqlName(nameNode, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
this.ctx.markUnresolvedType(nameNode, typeName); | ||
return typeName; | ||
}; | ||
Extractor.prototype.namedFunctionExportName = function (node) { | ||
var _a, _b; | ||
if (node.name == null) { | ||
return this.report(node, "Expected type extension function to be a named function."); | ||
} | ||
var exportKeyword = (_a = node.modifiers) === null || _a === void 0 ? void 0 : _a.some(function (modifier) { | ||
return modifier.kind === ts.SyntaxKind.ExportKeyword; | ||
}); | ||
var defaultKeyword = (_b = node.modifiers) === null || _b === void 0 ? void 0 : _b.find(function (modifier) { | ||
return modifier.kind === ts.SyntaxKind.DefaultKeyword; | ||
}); | ||
if (defaultKeyword != null) { | ||
// TODO: We could support this | ||
return this.report(defaultKeyword, "Expected type extension function to be a named export, not a default export."); | ||
} | ||
if (exportKeyword == null) { | ||
return this.report(node.name, "Expected type extension function to be a named export."); | ||
} | ||
return node.name; | ||
}; | ||
Extractor.prototype.scalarTypeAliasDeclaration = function (node, tag) { | ||
@@ -273,4 +378,3 @@ var name = this.entityName(node, tag); | ||
if (!ts.isTypeLiteralNode(node.type)) { | ||
this.reportUnhandled(node, "`@".concat(INPUT_TAG, "` can only be used on type literals.")); | ||
return null; | ||
return this.reportUnhandled(node, "`@".concat(INPUT_TAG, "` can only be used on type literals.")); | ||
} | ||
@@ -303,4 +407,3 @@ try { | ||
if (node.type == null) { | ||
this.report(node, "Input field must have a type annotation."); | ||
return null; | ||
return this.report(node, "Input field must have a type annotation."); | ||
} | ||
@@ -324,4 +427,3 @@ var inner = this.collectType(node.type); | ||
if (node.name == null) { | ||
this.report(node, "Unexpected `@".concat(TYPE_TAG, "` annotation on unnamed class declaration.")); | ||
return null; | ||
return this.report(node, "Unexpected `@".concat(TYPE_TAG, "` annotation on unnamed class declaration.")); | ||
} | ||
@@ -345,2 +447,20 @@ var name = this.entityName(node, tag); | ||
}; | ||
Extractor.prototype.typeInterfaceDeclaration = function (node, tag) { | ||
var name = this.entityName(node, tag); | ||
if (name == null) | ||
return null; | ||
var description = this.collectDescription(node.name); | ||
var fields = this.collectFields(node); | ||
var interfaces = this.collectInterfaces(node); | ||
this.ctx.recordTypeName(node.name, name.value); | ||
this.definitions.push({ | ||
kind: graphql_1.Kind.OBJECT_TYPE_DEFINITION, | ||
loc: this.loc(node), | ||
description: description !== null && description !== void 0 ? description : undefined, | ||
directives: undefined, | ||
name: name, | ||
fields: fields, | ||
interfaces: interfaces !== null && interfaces !== void 0 ? interfaces : undefined | ||
}); | ||
}; | ||
Extractor.prototype.collectInterfaces = function (node) { | ||
@@ -359,3 +479,3 @@ var _this = this; | ||
var namedType = _this.gqlNamedType(type.expression, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
_this.ctx.markUnresolvedType(type.expression, namedType); | ||
_this.ctx.markUnresolvedType(type.expression, namedType.name); | ||
return namedType; | ||
@@ -410,13 +530,8 @@ }); | ||
}; | ||
Extractor.prototype.collectArgs = function (node) { | ||
Extractor.prototype.collectArgs = function (argsParam) { | ||
var e_4, _a; | ||
var args = []; | ||
var argsParam = node.parameters[0]; | ||
if (argsParam == null) { | ||
return null; | ||
} | ||
var argsType = argsParam.type; | ||
if (argsType == null) { | ||
this.report(argsParam, "Expected GraphQL field arguments to have a TypeScript type. If there are no arguments, you can use `args: never`."); | ||
return null; | ||
return this.report(argsParam, "Expected GraphQL field arguments to have a TypeScript type. If there are no arguments, you can use `args: never`."); | ||
} | ||
@@ -427,4 +542,3 @@ if (argsType.kind === ts.SyntaxKind.NeverKeyword) { | ||
if (!ts.isTypeLiteralNode(argsType)) { | ||
this.report(argsType, "Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`."); | ||
return null; | ||
return this.report(argsType, "Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`."); | ||
} | ||
@@ -493,13 +607,10 @@ var defaults = null; | ||
// TODO: How can I create this error? | ||
this.report(node, "Expected GraphQL field argument type to be a property signature."); | ||
return null; | ||
return this.report(node, "Expected GraphQL field argument type to be a property signature."); | ||
} | ||
if (!ts.isIdentifier(node.name)) { | ||
// TODO: How can I create this error? | ||
this.report(node.name, "Expected GraphQL field argument names to be a literal."); | ||
return null; | ||
return this.report(node.name, "Expected GraphQL field argument names to be a literal."); | ||
} | ||
if (node.type == null) { | ||
this.report(node.name, "Expected GraphQL field argument to have a type."); | ||
return null; | ||
return this.report(node.name, "Expected GraphQL field argument to have a type."); | ||
} | ||
@@ -629,4 +740,3 @@ var type = this.collectType(node.type); | ||
if (node.name == null) { | ||
this.report(node, "Expected GraphQL entity to have a name."); | ||
return null; | ||
return this.report(node, "Expected GraphQL entity to have a name."); | ||
} | ||
@@ -646,4 +756,3 @@ var id = this.expectIdentifier(node.name); | ||
if (node.type == null) { | ||
this.report(node.name, "Expected GraphQL field to have a type."); | ||
return null; | ||
return this.report(node.name, "Expected GraphQL field to have a type."); | ||
} | ||
@@ -654,3 +763,7 @@ var type = this.collectType(node.type); | ||
return null; | ||
var args = this.collectArgs(node); | ||
var args = null; | ||
var argsParam = node.parameters[0]; | ||
if (argsParam != null) { | ||
args = this.collectArgs(argsParam); | ||
} | ||
var description = this.collectDescription(node.name); | ||
@@ -681,4 +794,3 @@ var id = this.expectIdentifier(node.name); | ||
if (symbol == null) { | ||
this.report(node, "Expected TypeScript to be able to resolve this GraphQL entity to a symbol."); | ||
return null; | ||
return this.report(node, "Expected TypeScript to be able to resolve this GraphQL entity to a symbol."); | ||
} | ||
@@ -785,8 +897,6 @@ var doc = symbol.getDocumentationComment(this.ctx.checker); | ||
else if (node.kind === ts.SyntaxKind.NumberKeyword) { | ||
this.report(node, "Unexpected number type. GraphQL supports both Int and Float, making `number` ambiguous. Instead, import the `Int` or `Float` type from `".concat(LIBRARY_IMPORT_NAME, "` and use that.")); | ||
return null; | ||
return this.report(node, "Unexpected number type. GraphQL supports both Int and Float, making `number` ambiguous. Instead, import the `Int` or `Float` type from `".concat(LIBRARY_IMPORT_NAME, "` and use that.")); | ||
} | ||
else if (ts.isTypeLiteralNode(node)) { | ||
this.report(node, "Unexpected type literal. You may want to define a named GraphQL type elsewhere and reference it here."); | ||
return null; | ||
return this.report(node, "Unexpected type literal. You may want to define a named GraphQL type elsewhere and reference it here."); | ||
} | ||
@@ -805,4 +915,3 @@ // TODO: Better error message. This is okay if it's a type reference, but everything else is not. | ||
if (node.typeArguments == null) { | ||
this.report(node, "Expected type reference to have type arguments."); | ||
return null; | ||
return this.report(node, "Expected type reference to have type arguments."); | ||
} | ||
@@ -818,4 +927,3 @@ var type = this.collectType(node.typeArguments[0]); | ||
if (node.typeArguments == null) { | ||
this.report(node, "Expected type reference to have type arguments."); | ||
return null; | ||
return this.report(node, "Expected type reference to have type arguments."); | ||
} | ||
@@ -833,3 +941,3 @@ var element = this.collectType(node.typeArguments[0]); | ||
var namedType = this.gqlNamedType(node, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
this.ctx.markUnresolvedType(node.typeName, namedType); | ||
this.ctx.markUnresolvedType(node.typeName, namedType.name); | ||
return this.gqlNonNullType(node, namedType); | ||
@@ -855,4 +963,3 @@ } | ||
} | ||
this.report(node, "Expected an identifier."); | ||
return null; | ||
return this.report(node, "Expected an identifier."); | ||
}; | ||
@@ -870,3 +977,3 @@ Extractor.prototype.findTag = function (node, tagName) { | ||
Extractor.prototype.handleErrorBubbling = function (type) { | ||
if (this.buildOptions.nullableByDefault) { | ||
if (this.configOptions.nullableByDefault) { | ||
return this.gqlNullableType(type); | ||
@@ -881,2 +988,8 @@ } | ||
}; | ||
Extractor.prototype.exportDirective = function (nameNode, filename, functionName) { | ||
return this.gqlConstDirective(nameNode, this.gqlName(nameNode, serverDirectives_1.EXPORTED_DIRECTIVE), [ | ||
this.gqlConstArgument(nameNode, this.gqlName(nameNode, serverDirectives_1.EXPORTED_FILENAME_ARG), this.gqlString(nameNode, filename)), | ||
this.gqlConstArgument(nameNode, this.gqlName(nameNode, serverDirectives_1.EXPORTED_FUNCTION_NAME_ARG), this.gqlString(nameNode, functionName)), | ||
]); | ||
}; | ||
/** GraphQL AST node helper methods */ | ||
@@ -883,0 +996,0 @@ Extractor.prototype.gqlName = function (node, value) { |
import { GraphQLSchema } from "graphql"; | ||
import { BuildOptions } from "./lib"; | ||
import * as ts from "typescript"; | ||
import { GratsOptions } from "./lib"; | ||
export * from "./Types"; | ||
export * from "./lib"; | ||
export declare function buildSchema(options: BuildOptions): GraphQLSchema; | ||
type RuntimeOptions = { | ||
emitSchemaFile?: string; | ||
}; | ||
export declare function extractGratsSchemaAtRuntime(runtimeOptions: RuntimeOptions): GraphQLSchema; | ||
export declare function buildSchemaFromSDL(sdlFilePath: string): GraphQLSchema; | ||
export declare function gratsOptionsFromTsConfig(tsConfig: ts.ParsedCommandLine): GratsOptions; | ||
export declare function getParsedTsConfig(): ts.ParsedCommandLine; |
@@ -17,6 +17,7 @@ "use strict"; | ||
exports.__esModule = true; | ||
exports.buildSchemaFromSDL = exports.buildSchema = void 0; | ||
exports.getParsedTsConfig = exports.gratsOptionsFromTsConfig = exports.buildSchemaFromSDL = exports.extractGratsSchemaAtRuntime = void 0; | ||
var graphql_1 = require("graphql"); | ||
var utils_1 = require("@graphql-tools/utils"); | ||
var fs = require("fs"); | ||
var ts = require("typescript"); | ||
var lib_1 = require("./lib"); | ||
@@ -28,5 +29,6 @@ __exportStar(require("./Types"), exports); | ||
// message. | ||
function buildSchema(options) { | ||
function extractGratsSchemaAtRuntime(runtimeOptions) { | ||
var _a; | ||
var schemaResult = (0, lib_1.buildSchemaResult)(options); | ||
var parsedTsConfig = getParsedTsConfig(); | ||
var schemaResult = (0, lib_1.buildSchemaResult)(gratsOptionsFromTsConfig(parsedTsConfig)); | ||
if (schemaResult.kind === "ERROR") { | ||
@@ -37,6 +39,6 @@ console.error(schemaResult.err.formatDiagnosticsWithColorAndContext()); | ||
var runtimeSchema = schemaResult.value; | ||
if (options.emitSchemaFile) { | ||
if (runtimeOptions.emitSchemaFile) { | ||
runtimeSchema = (0, graphql_1.lexicographicSortSchema)(runtimeSchema); | ||
var sdl = (0, utils_1.printSchemaWithDirectives)(runtimeSchema, { assumeValid: true }); | ||
var filePath = (_a = options.emitSchemaFile) !== null && _a !== void 0 ? _a : "./schema.graphql"; | ||
var filePath = (_a = runtimeOptions.emitSchemaFile) !== null && _a !== void 0 ? _a : "./schema.graphql"; | ||
fs.writeFileSync(filePath, sdl); | ||
@@ -46,3 +48,3 @@ } | ||
} | ||
exports.buildSchema = buildSchema; | ||
exports.extractGratsSchemaAtRuntime = extractGratsSchemaAtRuntime; | ||
function buildSchemaFromSDL(sdlFilePath) { | ||
@@ -54,1 +56,26 @@ var sdl = fs.readFileSync(sdlFilePath, "utf8"); | ||
exports.buildSchemaFromSDL = buildSchemaFromSDL; | ||
function gratsOptionsFromTsConfig(tsConfig) { | ||
var _a; | ||
var gratsConfig = (_a = tsConfig.raw.grats) !== null && _a !== void 0 ? _a : {}; | ||
return { | ||
configOptions: gratsConfig, | ||
files: tsConfig.fileNames, | ||
tsCompilerOptions: tsConfig.options | ||
}; | ||
} | ||
exports.gratsOptionsFromTsConfig = gratsOptionsFromTsConfig; | ||
// #FIXME: Report diagnostics instead of throwing! | ||
function getParsedTsConfig() { | ||
var configFile = ts.findConfigFile(process.cwd(), ts.sys.fileExists); | ||
if (!configFile) { | ||
throw new Error("Grats: Could not find tsconfig.json"); | ||
} | ||
// https://github.com/microsoft/TypeScript/blob/46d70d79cd0dd00d19e4c617d6ebb25e9f3fc7de/src/compiler/watch.ts#L216 | ||
var configFileHost = ts.sys; | ||
var parsed = ts.getParsedCommandLineOfConfigFile(configFile, undefined, configFileHost); | ||
if (!parsed || parsed.errors.length > 0) { | ||
throw new Error("Grats: Could not parse tsconfig.json"); | ||
} | ||
return parsed; | ||
} | ||
exports.getParsedTsConfig = getParsedTsConfig; |
@@ -5,9 +5,12 @@ import { DocumentNode, GraphQLSchema } from "graphql"; | ||
export { applyServerDirectives } from "./serverDirectives"; | ||
export type BuildOptions = { | ||
files: string[]; | ||
emitSchemaFile?: string; | ||
export type ConfigOptions = { | ||
nullableByDefault?: boolean; | ||
}; | ||
export declare function buildSchemaResult(options: BuildOptions): Result<GraphQLSchema, ReportableDiagnostics>; | ||
export declare function buildSchemaResultWithHost(options: BuildOptions, compilerOptions: ts.CompilerOptions, compilerHost: ts.CompilerHost): Result<GraphQLSchema, ReportableDiagnostics>; | ||
export declare function buildSchemaAst(options: BuildOptions, host: ts.CompilerHost, compilerOptions: ts.CompilerOptions): DiagnosticsResult<DocumentNode>; | ||
export type GratsOptions = { | ||
configOptions: ConfigOptions; | ||
tsCompilerOptions: ts.CompilerOptions; | ||
files: string[]; | ||
}; | ||
export declare function buildSchemaResult(options: GratsOptions): Result<GraphQLSchema, ReportableDiagnostics>; | ||
export declare function buildSchemaResultWithHost(options: GratsOptions, compilerHost: ts.CompilerHost): Result<GraphQLSchema, ReportableDiagnostics>; | ||
export declare function buildSchemaAst(options: GratsOptions, host: ts.CompilerHost): DiagnosticsResult<DocumentNode>; |
@@ -39,11 +39,10 @@ "use strict"; | ||
// https://stackoverflow.com/a/66604532/1263117 | ||
var compilerOptions = { allowJs: true }; | ||
var compilerHost = ts.createCompilerHost(options, | ||
var compilerHost = ts.createCompilerHost(options.tsCompilerOptions, | ||
/* setParentNodes this is needed for finding jsDocs */ | ||
true); | ||
return buildSchemaResultWithHost(options, compilerOptions, compilerHost); | ||
return buildSchemaResultWithHost(options, compilerHost); | ||
} | ||
exports.buildSchemaResult = buildSchemaResult; | ||
function buildSchemaResultWithHost(options, compilerOptions, compilerHost) { | ||
var docResult = buildSchemaAst(options, compilerHost, compilerOptions); | ||
function buildSchemaResultWithHost(options, compilerHost) { | ||
var docResult = buildSchemaAst(options, compilerHost); | ||
if (docResult.kind === "ERROR") { | ||
@@ -63,4 +62,4 @@ return (0, DiagnosticError_1.err)(new DiagnosticError_1.ReportableDiagnostics(compilerHost, docResult.err)); | ||
exports.buildSchemaResultWithHost = buildSchemaResultWithHost; | ||
function buildSchemaAst(options, host, compilerOptions) { | ||
var docResult = definitionsFromFile(options, host, compilerOptions); | ||
function buildSchemaAst(options, host) { | ||
var docResult = definitionsFromFile(options, host); | ||
if (docResult.kind === "ERROR") | ||
@@ -82,5 +81,5 @@ return docResult; | ||
exports.buildSchemaAst = buildSchemaAst; | ||
function definitionsFromFile(options, host, compilerOptions) { | ||
function definitionsFromFile(options, host) { | ||
var e_1, _a, e_2, _b; | ||
var program = ts.createProgram(options.files, compilerOptions, host); | ||
var program = ts.createProgram(options.files, options.tsCompilerOptions, host); | ||
var checker = program.getTypeChecker(); | ||
@@ -96,3 +95,3 @@ var ctx = new TypeContext_1.TypeContext(checker, host); | ||
} | ||
var extractor = new Extractor_1.Extractor(sourceFile, ctx, options); | ||
var extractor = new Extractor_1.Extractor(sourceFile, ctx, options.configOptions); | ||
var extractedResult = extractor.extract(); | ||
@@ -99,0 +98,0 @@ if (extractedResult.kind === "ERROR") |
import { GraphQLSchema } from "graphql"; | ||
export declare const METHOD_NAME_DIRECTIVE = "methodName"; | ||
export declare const METHOD_NAME_ARG = "name"; | ||
export declare const EXPORTED_DIRECTIVE = "exported"; | ||
export declare const EXPORTED_FILENAME_ARG = "filename"; | ||
export declare const EXPORTED_FUNCTION_NAME_ARG = "functionName"; | ||
export declare const DIRECTIVES_AST: import("graphql").DocumentNode; | ||
/** | ||
* Field renaming directive: | ||
* | ||
* By default, when resolving a field, the server will take the schema field | ||
* name, and look for a resolver/property by that name on the parent object. | ||
* Since we support exposing a method/property under a different name, we need | ||
* to modify that field's resolver to look for the implementation name rather | ||
* than the schema name. | ||
*/ | ||
export declare function applyServerDirectives(schema: GraphQLSchema): GraphQLSchema; |
@@ -13,18 +13,49 @@ "use strict"; | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (g && (g = 0, op[0] && (_ = 0)), _) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [op[0] & 2, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
exports.__esModule = true; | ||
exports.applyServerDirectives = exports.DIRECTIVES_AST = exports.METHOD_NAME_ARG = exports.METHOD_NAME_DIRECTIVE = void 0; | ||
exports.applyServerDirectives = exports.DIRECTIVES_AST = exports.EXPORTED_FUNCTION_NAME_ARG = exports.EXPORTED_FILENAME_ARG = exports.EXPORTED_DIRECTIVE = exports.METHOD_NAME_ARG = exports.METHOD_NAME_DIRECTIVE = void 0; | ||
var utils_1 = require("@graphql-tools/utils"); | ||
var graphql_1 = require("graphql"); | ||
var path_1 = require("path"); | ||
exports.METHOD_NAME_DIRECTIVE = "methodName"; | ||
exports.METHOD_NAME_ARG = "name"; | ||
exports.DIRECTIVES_AST = (0, graphql_1.parse)("\n directive @".concat(exports.METHOD_NAME_DIRECTIVE, "(").concat(exports.METHOD_NAME_ARG, ": String!) on FIELD_DEFINITION\n")); | ||
/** | ||
* Field renaming directive: | ||
* | ||
* By default, when resolving a field, the server will take the schema field | ||
* name, and look for a resolver/property by that name on the parent object. | ||
* Since we support exposing a method/property under a different name, we need | ||
* to modify that field's resolver to look for the implementation name rather | ||
* than the schema name. | ||
*/ | ||
exports.EXPORTED_DIRECTIVE = "exported"; | ||
exports.EXPORTED_FILENAME_ARG = "filename"; | ||
exports.EXPORTED_FUNCTION_NAME_ARG = "functionName"; | ||
exports.DIRECTIVES_AST = (0, graphql_1.parse)("\n directive @".concat(exports.METHOD_NAME_DIRECTIVE, "(").concat(exports.METHOD_NAME_ARG, ": String!) on FIELD_DEFINITION\n directive @").concat(exports.EXPORTED_DIRECTIVE, "(\n ").concat(exports.EXPORTED_FILENAME_ARG, ": String!,\n ").concat(exports.EXPORTED_FUNCTION_NAME_ARG, ": String!\n ) on FIELD_DEFINITION\n")); | ||
function applyServerDirectives(schema) { | ||
@@ -36,11 +67,13 @@ var _a; | ||
_a[utils_1.MapperKind.OBJECT_FIELD] = function (fieldConfig) { | ||
var _a; | ||
var _a, _b; | ||
var newFieldConfig = fieldConfig; | ||
var methodNameDirective = (_a = (0, utils_1.getDirective)(schema, fieldConfig, exports.METHOD_NAME_DIRECTIVE)) === null || _a === void 0 ? void 0 : _a[0]; | ||
if (!methodNameDirective) | ||
return; | ||
var _b = fieldConfig.resolve, resolve = _b === void 0 ? graphql_1.defaultFieldResolver : _b; | ||
return __assign(__assign({}, fieldConfig), { resolve: function (source, args, context, info) { | ||
var newInfo = __assign(__assign({}, info), { fieldName: methodNameDirective[exports.METHOD_NAME_ARG] }); | ||
return resolve(source, args, context, newInfo); | ||
} }); | ||
if (methodNameDirective != null) { | ||
newFieldConfig = applyMethodNameDirective(newFieldConfig, methodNameDirective); | ||
} | ||
var exportedDirective = (_b = (0, utils_1.getDirective)(schema, fieldConfig, exports.EXPORTED_DIRECTIVE)) === null || _b === void 0 ? void 0 : _b[0]; | ||
if (exportedDirective != null) { | ||
newFieldConfig = applyExportDirective(newFieldConfig, exportedDirective); | ||
} | ||
return newFieldConfig; | ||
}, | ||
@@ -50,1 +83,59 @@ _a)); | ||
exports.applyServerDirectives = applyServerDirectives; | ||
/** | ||
* Field renaming directive: | ||
* | ||
* By default, when resolving a field, the server will take the schema field | ||
* name, and look for a resolver/property by that name on the parent object. | ||
* Since we support exposing a method/property under a different name, we need | ||
* to modify that field's resolver to look for the implementation name rather | ||
* than the schema name. | ||
*/ | ||
function applyMethodNameDirective(fieldConfig, methodNameDirective) { | ||
var _a = fieldConfig.resolve, resolve = _a === void 0 ? graphql_1.defaultFieldResolver : _a; | ||
return __assign(__assign({}, fieldConfig), { resolve: function (source, args, context, info) { | ||
var newInfo = __assign(__assign({}, info), { fieldName: methodNameDirective[exports.METHOD_NAME_ARG] }); | ||
return resolve(source, args, context, newInfo); | ||
} }); | ||
} | ||
/** | ||
* Export directive: | ||
* | ||
* By default, when resolving a field, the server will look for a resolver | ||
* function on the parent object. This directive allows you to specify a | ||
* module and function name to import and use as the resolver. | ||
*/ | ||
function applyExportDirective(fieldConfig, methodNameDirective) { | ||
// FIXME: This relies on the server being run with the same cwd as the build script. | ||
// TODO: Does this work in the browser? | ||
var filename = (0, path_1.resolve)(process.cwd(), methodNameDirective[exports.EXPORTED_FILENAME_ARG]); | ||
var functionName = methodNameDirective[exports.EXPORTED_FUNCTION_NAME_ARG]; | ||
return __assign(__assign({}, fieldConfig), { resolve: function (source, args, context, info) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var mod, e_1, resolve; | ||
return __generator(this, function (_a) { | ||
var _b; | ||
switch (_a.label) { | ||
case 0: | ||
mod = {}; | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, (_b = filename, Promise.resolve().then(function () { return require(_b); }))]; | ||
case 2: | ||
mod = _a.sent(); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
e_1 = _a.sent(); | ||
console.error("Grats Error: Failed to import module `".concat(filename, "`. You may need to rerun Grats.")); | ||
throw e_1; | ||
case 4: | ||
resolve = mod[functionName]; | ||
if (typeof resolve !== "function") { | ||
throw new Error("Grats Error: Expected `".concat(filename, "` to have a named export `").concat(functionName, "` that is a function, but it was `").concat(typeof resolve, "`. You may need to rerun Grats.")); | ||
} | ||
return [2 /*return*/, resolve(source, args, context, info)]; | ||
} | ||
}); | ||
}); | ||
} }); | ||
} |
@@ -127,3 +127,8 @@ "use strict"; | ||
var files = ["".concat(fixturesDir, "/").concat(fileName), "src/Types.ts"]; | ||
var schemaResult = (0, lib_1.buildSchemaResult)(__assign({ files: files }, options)); | ||
var parsedOptions = { | ||
tsCompilerOptions: {}, | ||
files: files, | ||
configOptions: options | ||
}; | ||
var schemaResult = (0, lib_1.buildSchemaResult)(parsedOptions); | ||
if (schemaResult.kind === "ERROR") { | ||
@@ -130,0 +135,0 @@ return schemaResult.err.formatDiagnosticsWithContext(); |
@@ -1,2 +0,2 @@ | ||
import { DocumentNode, NamedTypeNode } from "graphql"; | ||
import { DocumentNode, NameNode } from "graphql"; | ||
import * as ts from "typescript"; | ||
@@ -21,8 +21,8 @@ import { DiagnosticResult, DiagnosticsResult } from "./utils/DiagnosticError"; | ||
_symbolToName: Map<ts.Symbol, string>; | ||
_unresolvedTypes: Map<NamedTypeNode, ts.Symbol>; | ||
_unresolvedTypes: Map<NameNode, ts.Symbol>; | ||
constructor(checker: ts.TypeChecker, host: ts.CompilerHost); | ||
recordTypeName(node: ts.Node, name: string): void; | ||
markUnresolvedType(node: ts.Node, namedType: NamedTypeNode): void; | ||
markUnresolvedType(node: ts.Node, name: NameNode): void; | ||
resolveTypes(doc: DocumentNode): DiagnosticsResult<DocumentNode>; | ||
resolveNamedType(namedType: NamedTypeNode): DiagnosticResult<NamedTypeNode>; | ||
resolveNamedType(unresolved: NameNode): DiagnosticResult<NameNode>; | ||
} |
@@ -50,5 +50,6 @@ "use strict"; | ||
}; | ||
TypeContext.prototype.markUnresolvedType = function (node, namedType) { | ||
TypeContext.prototype.markUnresolvedType = function (node, name) { | ||
var symbol = this.checker.getSymbolAtLocation(node); | ||
if (symbol == null) { | ||
// | ||
throw new Error("Could not resolve type reference. You probably have a TypeScript error."); | ||
@@ -60,3 +61,3 @@ } | ||
} | ||
this._unresolvedTypes.set(namedType, symbol); | ||
this._unresolvedTypes.set(name, symbol); | ||
}; | ||
@@ -67,3 +68,3 @@ TypeContext.prototype.resolveTypes = function (doc) { | ||
var newDoc = (0, graphql_1.visit)(doc, { | ||
NamedType: function (t) { | ||
Name: function (t) { | ||
var namedTypeResult = _this.resolveNamedType(t); | ||
@@ -82,14 +83,14 @@ if (namedTypeResult.kind === "ERROR") { | ||
}; | ||
TypeContext.prototype.resolveNamedType = function (namedType) { | ||
var symbol = this._unresolvedTypes.get(namedType); | ||
TypeContext.prototype.resolveNamedType = function (unresolved) { | ||
var symbol = this._unresolvedTypes.get(unresolved); | ||
if (symbol == null) { | ||
if (namedType.name.value === exports.UNRESOLVED_REFERENCE_NAME) { | ||
if (unresolved.value === exports.UNRESOLVED_REFERENCE_NAME) { | ||
// This is a logic error on our side. | ||
throw new Error("Unexpected unresolved reference name."); | ||
} | ||
return (0, DiagnosticError_1.ok)(namedType); | ||
return (0, DiagnosticError_1.ok)(unresolved); | ||
} | ||
var name = this._symbolToName.get(symbol); | ||
if (name == null) { | ||
if (namedType.loc == null) { | ||
if (unresolved.loc == null) { | ||
throw new Error("Expected namedType to have a location."); | ||
@@ -99,10 +100,10 @@ } | ||
messageText: "This type is not a valid GraphQL type. Did you mean to annotate it's definition with `/** @GQLType */` or `/** @GQLScalar */`?", | ||
start: namedType.loc.start, | ||
length: namedType.loc.end - namedType.loc.start, | ||
start: unresolved.loc.start, | ||
length: unresolved.loc.end - unresolved.loc.start, | ||
category: ts.DiagnosticCategory.Error, | ||
code: DiagnosticError_1.FAKE_ERROR_CODE, | ||
file: ts.createSourceFile(namedType.loc.source.name, namedType.loc.source.body, ts.ScriptTarget.Latest) | ||
file: ts.createSourceFile(unresolved.loc.source.name, unresolved.loc.source.body, ts.ScriptTarget.Latest) | ||
}); | ||
} | ||
return (0, DiagnosticError_1.ok)(__assign(__assign({}, namedType), { name: __assign(__assign({}, namedType.name), { value: name }) })); | ||
return (0, DiagnosticError_1.ok)(__assign(__assign({}, unresolved), { value: name })); | ||
}; | ||
@@ -109,0 +110,0 @@ return TypeContext; |
{ | ||
"name": "grats", | ||
"version": "0.0.0", | ||
"version": "0.0.1", | ||
"main": "dist/src/index.js", | ||
@@ -13,3 +13,2 @@ "bin": "dist/src/cli.js", | ||
"@graphql-tools/utils": "^9.2.1", | ||
"glob": "^9.2.1", | ||
"graphql": "^16.6.0", | ||
@@ -16,0 +15,0 @@ "typescript": "^4.9.5" |
201
README.md
@@ -1,16 +0,19 @@ | ||
# ------- EXPERIMENTAL PRE ALPHA ------ | ||
# -=[ EXPERIMENTAL PRE ALPHA ]=- | ||
**This is just a proof of concept right now. Everything is very thrown together and inefficient. If it encounters any AST nodes I haven't considered yet, it will simply error out.** | ||
**This is currently a proof of concept. It won't yet work on any real projects.** | ||
# TypeScript First GraphQL (Name to be determined) | ||
# Grats: True code-first GraphQL for TypeScript | ||
TypeScript First GraphQL is a library for deriving a GraphQL schema from | ||
your lightly-annotated TypeScript code. The goal is to make your TypeScript | ||
code, and its types, the source of truth for your GraphQL schema without | ||
requiring decorators or other special syntax/APIs. | ||
Grats is a tool for statically infering GraphQL schema from your vanilla | ||
TypeScript code. | ||
Opt classes and methods on your existing models into your GraphQL API by | ||
annotating them with a simple comment. TypeScript First GraphQL will derive the | ||
GraphQL schema for you via static analysis. | ||
Just write your types and resolvers as regular TypeScript and annotate your | ||
types and fields with simple JSDoc tags. From there, Grats can extract your | ||
GraphQL schema automatically by statically analyzing your code and its types. No | ||
convoluted directive APIs to remember. No need to define your Schema at | ||
runtime with verbose builder APIs. | ||
By making your TypeScript implementation the source of truth, you entirely | ||
remove the question of mismatches between your implementation and your GraphQL schema definition. Your implementation _is_ the schema definition! | ||
## Example | ||
@@ -64,21 +67,70 @@ | ||
## CLI Usage | ||
**Give it a try in the [online playground](https://capt.dev/grats-example)!** | ||
Still very rough, but you can try it out with: | ||
## Quick Start | ||
*Note:* Globs will be evaluated relative to the current working directory. | ||
For dev mode or small projects, Grats offers a runtime extraction mode. This is | ||
the easiest way to get started with Grats, although you may find that it causes | ||
a slow startup time. For larger projects, you probably want to use the build | ||
mode (documentation to come). | ||
```bash | ||
git clone ... | ||
pnpm install | ||
pnpm cli <glob of files to analyze> | ||
```sh | ||
npm install express express-graphql grats | ||
``` | ||
# Example: `pnpm cli "example-server/**/*.ts"` | ||
**Ensure your project has a `tsconfig.json` file.** | ||
```ts | ||
import * as express from "express"; | ||
import { graphqlHTTP } from "express-graphql"; | ||
import { extractGratsSchemaAtRuntime } from "grats"; | ||
/** @GQLType */ | ||
class Query { | ||
/** @GQLField */ | ||
hello(): string { | ||
return "Hello world!"; | ||
} | ||
} | ||
const app = express(); | ||
// At runtime Grats will parse your TypeScript project (including this file!) and | ||
// extract the GraphQL schema. | ||
const schema = extractGratsSchemaAtRuntime({ | ||
emitSchemaFile: "./schema.graphql", | ||
}); | ||
app.use( | ||
"/graphql", | ||
graphqlHTTP({ schema, rootValue: new Query(), graphiql: true }), | ||
); | ||
app.listen(4000); | ||
console.log("Running a GraphQL API server at http://localhost:4000/graphql"); | ||
``` | ||
## Configuration | ||
Grats has a few configuration options. They can be set in your project's | ||
`tsconfig.json` file: | ||
```json | ||
{ | ||
"grats": { | ||
// Should all fields be typed as nullable in accordance with GraphQL best | ||
// practices? | ||
// https://graphql.org/learn/best-practices/#nullability | ||
"nullableByDefault": true, // Default: true | ||
}, | ||
"compilerOptions": { | ||
// ... TypeScript config... | ||
} | ||
} | ||
``` | ||
## API Usage | ||
In order for TypeScript First GraphQL to be able to extract GraphQL schema from | ||
your code, simply mark which classes and methods should be included in the schema by | ||
marking them with special JSDoc tags such as `/** @GQLType */` or `/** @GQLField */`. | ||
In order for Grats to extract GraphQL schema from your code, simply mark which | ||
TypeScript structures should be included in the schema by marking them with | ||
special JSDoc tags such as `/** @GQLType */` or `/** @GQLField */`. | ||
@@ -90,2 +142,14 @@ Any comment text preceding the JSDoc `@` tag will be used as that element's description. | ||
The following JSDoc tags are supported: | ||
* [`@GQLType`](#GQLtype) | ||
* [`@GQLInterface`](#GQLinterface) | ||
* [`@GQLField`](#GQLfield) | ||
* [`@GQLUnion`](#GQLunion) | ||
* [`@GQLScalar`](#GQLscalar) | ||
* [`@GQLEnum`](#GQLenum) | ||
* [`@GQLInput`](#GQLinput) | ||
* [`@GQLExtendType`](#GQLExtendType) | ||
### @GQLType | ||
@@ -96,2 +160,3 @@ | ||
* Class declaration | ||
* Interface declaration | ||
@@ -109,2 +174,13 @@ ```ts | ||
```ts | ||
/** | ||
* Here I can write a description of my type that will be included in the schema. | ||
* @GQLType <optional name of the type, if different from interface name> | ||
*/ | ||
interface MyInterface { | ||
/** @GQLField */ | ||
someField: string; | ||
} | ||
``` | ||
### @GQLInterface | ||
@@ -154,3 +230,4 @@ | ||
**Note**: TypeScript First GraphQL makes all fields nullable by default in keeping with [GraphQL best practices](https://graphql.org/learn/best-practices/#nullability). In the future, we should make this configurable. | ||
**Note**: By default, Grats makes all fields nullable in keeping with [GraphQL | ||
*best practices](https://graphql.org/learn/best-practices/#nullability). This behavior can be changed by setting config option `nullableByDefault` to `false`. | ||
@@ -227,2 +304,16 @@ If you wish to define arguments for a field, define your argument types inline: | ||
Note: For the built-in GraphQL scalars that don't have a corresponding TypeScript type, Grats ships with type aliases you can import. You may be promted to use one of these by Grat if you try to use `number` in a positon from which Grat needs to infer a GraphQL type. | ||
```ts | ||
import { Float, Int } from "grats"; | ||
/** @GQLType */ | ||
class Query { | ||
/** @GQLField */ | ||
round(args: {float: Float}): Int { | ||
return Math.round(args.float); | ||
} | ||
} | ||
``` | ||
### @GQLEnum | ||
@@ -263,3 +354,3 @@ | ||
We also support defining enums using a union of string literals, howerver there | ||
We also support defining enums using a union of string literals, however there | ||
are some limitations to this approach: | ||
@@ -298,2 +389,45 @@ | ||
### @GQLExtendType | ||
Sometimes you want to add a computed field to a non-class type, or extend base | ||
type like `Query` or `Mutation` from another file. Both of these usecases are | ||
enabled by placing a `@GQLExtendType` before a: | ||
* Exported function declaration | ||
In this case, the function should expect an instance of the base type as the | ||
first argument, and an object representing the GraphQL field arguments as the | ||
second argument. The function should return the value of the field. | ||
Extending Query: | ||
```ts | ||
/** | ||
* Description of my field | ||
* @GQLExtendType <optional name of the field, if different from function name> | ||
*/ | ||
export function userById(_: Query, args: {id: string}): User { | ||
return DB.getUserById(args.id); | ||
} | ||
``` | ||
Extending Mutation: | ||
```ts | ||
/** | ||
* Delete a user. GOODBYE! | ||
* @GQLExtendType <optional name of the mutation, if different from function name> | ||
*/ | ||
export function deleteUser(_: Mutation, args: {id: string}): boolean { | ||
return DB.deleteUser(args.id); | ||
} | ||
``` | ||
Note that Grats will use the type of the first argument to determine which type | ||
is being extended. So, as seen in the previous examples, even if you don't need | ||
access to the instance you should still define a typed first argument. | ||
You can think of `@GQLExtendType` as equivalent to the `extend type` syntax in | ||
GraphQL's schema definition language. | ||
## Example | ||
@@ -312,10 +446,15 @@ | ||
## Why would I _not_ want to use TypeScript First GraphQL? | ||
## Why would I _not_ want to use Grats | ||
Because TypeScript First GraphQL relies on static analysis to infer types, it | ||
requires that your GraphQL fields use types that can be statically analyzed. | ||
This means that you can't use complex derived types in positions where | ||
TypeScript First GraphQL needs to be able to infer the type. For example, field | ||
arguments and return values. | ||
Because Grats relies on static analysis to infer types, it requires that your | ||
GraphQL fields use types that can be statically analyzed. This means that you | ||
can't use complex derived types in positions where Grats needs to be able to | ||
infer the type. For example, field arguments and return values. | ||
Currently, Grats does not have a great way to handle the case where you want to | ||
expose structures that are not owned by your codebase. For example, if you want | ||
to expose a field that returns a type from a third-party library, or a type that | ||
is generated by some other codegen tool. Today, your best option is to define a | ||
wrapper resolver class. | ||
## Why use comments and not decorators? | ||
@@ -340,3 +479,3 @@ | ||
the impression that they might have some runtime behavior. This is not the | ||
case for TypeScript First GraphQL, which is purely a static analysis tool. | ||
case for Grats, which is purely a static analysis tool. | ||
@@ -354,2 +493,2 @@ Given these tradeoffs, we've decided to use comments instead of decorators. | ||
* [ts2graphql](https://github.com/cevek/ts2graphql) | ||
* [typegraphql-reflection-poc](https://github.com/MichalLytek/typegraphql-reflection-poc) | ||
* [typegraphql-reflection-poc](https://github.com/MichalLytek/typegraphql-reflection-poc) |
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
117045
3
2258
484
4
- Removedglob@^9.2.1
- Removedbalanced-match@1.0.2(transitive)
- Removedbrace-expansion@2.0.1(transitive)
- Removedfs.realpath@1.0.0(transitive)
- Removedglob@9.3.5(transitive)
- Removedlru-cache@10.4.3(transitive)
- Removedminimatch@8.0.4(transitive)
- Removedminipass@4.2.87.1.2(transitive)
- Removedpath-scurry@1.11.1(transitive)