Comparing version 0.0.1 to 0.0.2
@@ -31,3 +31,2 @@ import { DefinitionNode, FieldDefinitionNode, InputValueDefinitionNode, ListTypeNode, NamedTypeNode, Location as GraphQLLocation, NameNode, Token, TypeNode, NonNullTypeNode, StringValueNode, ConstValueNode, ConstDirectiveNode, ConstArgumentNode, EnumValueDefinitionNode } from "graphql"; | ||
extractUnion(node: ts.Node, tag: ts.JSDocTag): void; | ||
extractExtendType(node: ts.Node, tag: ts.JSDocTag): void; | ||
/** Error handling and location juggling */ | ||
@@ -54,2 +53,7 @@ report(node: ts.Node, message: string): null; | ||
typeInterfaceDeclaration(node: ts.InterfaceDeclaration, tag: ts.JSDocTag): null | undefined; | ||
checkForTypenameProperty(node: ts.ClassDeclaration | ts.InterfaceDeclaration, expectedName: string): void; | ||
isValidTypeNameProperty(member: ts.ClassElement | ts.TypeElement, expectedName: string): boolean; | ||
isValidTypenamePropertyDeclaration(node: ts.PropertyDeclaration, expectedName: string): boolean; | ||
isValidTypenamePropertySignature(node: ts.PropertySignature, expectedName: string): boolean; | ||
isValidTypenamePropertyType(node: ts.TypeNode, expectedName: string): boolean; | ||
collectInterfaces(node: ts.ClassDeclaration | ts.InterfaceDeclaration): Array<NamedTypeNode> | null; | ||
@@ -65,4 +69,4 @@ interfaceInterfaceDeclaration(node: ts.InterfaceDeclaration, tag: ts.JSDocTag): void; | ||
collectEnumValues(node: ts.EnumDeclaration): ReadonlyArray<EnumValueDefinitionNode>; | ||
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; | ||
entityName(node: ts.ClassDeclaration | ts.MethodDeclaration | ts.MethodSignature | ts.PropertyDeclaration | ts.InterfaceDeclaration | ts.PropertySignature | ts.EnumDeclaration | ts.TypeAliasDeclaration | ts.FunctionDeclaration, tag: ts.JSDocTag): NameNode | null; | ||
methodDeclaration(node: ts.MethodDeclaration | ts.MethodSignature): FieldDefinitionNode | null; | ||
collectDescription(node: ts.Node): StringValueNode | null; | ||
@@ -69,0 +73,0 @@ collectDeprecated(node: ts.Node): ConstDirectiveNode | null; |
@@ -24,10 +24,18 @@ "use strict"; | ||
var ISSUE_URL = "https://github.com/captbaritone/grats/issues"; | ||
var TYPE_TAG = "GQLType"; | ||
var FIELD_TAG = "GQLField"; | ||
var SCALAR_TAG = "GQLScalar"; | ||
var INTERFACE_TAG = "GQLInterface"; | ||
var ENUM_TAG = "GQLEnum"; | ||
var UNION_TAG = "GQLUnion"; | ||
var INPUT_TAG = "GQLInput"; | ||
var EXTEND_TYPE = "GQLExtendType"; | ||
var TYPE_TAG = "gqlType"; | ||
var FIELD_TAG = "gqlField"; | ||
var SCALAR_TAG = "gqlScalar"; | ||
var INTERFACE_TAG = "gqlInterface"; | ||
var ENUM_TAG = "gqlEnum"; | ||
var UNION_TAG = "gqlUnion"; | ||
var INPUT_TAG = "gqlInput"; | ||
var ALL_TAGS = [ | ||
TYPE_TAG, | ||
FIELD_TAG, | ||
SCALAR_TAG, | ||
INTERFACE_TAG, | ||
ENUM_TAG, | ||
UNION_TAG, | ||
INPUT_TAG, | ||
]; | ||
var DEPRECATED_TAG = "deprecated"; | ||
@@ -59,6 +67,6 @@ /** | ||
ts.forEachChild(this.sourceFile, function (node) { | ||
var e_1, _a; | ||
var e_1, _a, e_2, _b; | ||
try { | ||
for (var _b = __values(ts.getJSDocTags(node)), _c = _b.next(); !_c.done; _c = _b.next()) { | ||
var tag = _c.value; | ||
for (var _c = __values(ts.getJSDocTags(node)), _d = _c.next(); !_d.done; _d = _c.next()) { | ||
var tag = _d.value; | ||
switch (tag.tagName.text) { | ||
@@ -71,8 +79,2 @@ case TYPE_TAG: | ||
break; | ||
case FIELD_TAG: | ||
// Right now this happens via deep traversal | ||
// TODO: Handle GQLField as part of this top level traversal | ||
// by keeping track of the current type we're in and appending fields | ||
// as we go. | ||
break; | ||
case INTERFACE_TAG: | ||
@@ -90,5 +92,37 @@ _this.extractInterface(node, tag); | ||
break; | ||
case EXTEND_TYPE: | ||
_this.extractExtendType(node, tag); | ||
case FIELD_TAG: | ||
if (ts.isFunctionDeclaration(node)) { | ||
_this.functionDeclarationExtendType(node, tag); | ||
} | ||
else if (!(ts.isMethodDeclaration(node) || | ||
ts.isPropertyDeclaration(node) || | ||
ts.isMethodSignature(node) || | ||
ts.isPropertySignature(node))) { | ||
// Right now this happens via deep traversal | ||
// Note: Keep this in sync with `collectFields` | ||
_this.reportUnhandled(node, "`@".concat(FIELD_TAG, "` can only be used on method/property declarations or signatures.")); | ||
} | ||
break; | ||
default: | ||
var lowerCaseTag = tag.tagName.text.toLowerCase(); | ||
if (lowerCaseTag.startsWith("gql")) { | ||
try { | ||
for (var ALL_TAGS_1 = (e_2 = void 0, __values(ALL_TAGS)), ALL_TAGS_1_1 = ALL_TAGS_1.next(); !ALL_TAGS_1_1.done; ALL_TAGS_1_1 = ALL_TAGS_1.next()) { | ||
var t = ALL_TAGS_1_1.value; | ||
if (t.toLowerCase() === lowerCaseTag) { | ||
_this.report(tag.tagName, "Incorrect casing for Grats tag `@".concat(tag.tagName.text, "`. Use `@").concat(t, "` instead.")); | ||
break; | ||
} | ||
} | ||
} | ||
catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
finally { | ||
try { | ||
if (ALL_TAGS_1_1 && !ALL_TAGS_1_1.done && (_b = ALL_TAGS_1["return"])) _b.call(ALL_TAGS_1); | ||
} | ||
finally { if (e_2) throw e_2.error; } | ||
} | ||
_this.report(tag.tagName, "`@".concat(tag.tagName.text, "` is not a valid Grats tag. Valid tags are: ").concat(ALL_TAGS.map(function (t) { return "`@".concat(t, "`"); }).join(", "), ".")); | ||
} | ||
break; | ||
} | ||
@@ -100,3 +134,3 @@ } | ||
try { | ||
if (_c && !_c.done && (_a = _b["return"])) _a.call(_b); | ||
if (_d && !_d.done && (_a = _c["return"])) _a.call(_c); | ||
} | ||
@@ -165,11 +199,2 @@ finally { if (e_1) throw e_1.error; } | ||
}; | ||
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 */ | ||
@@ -217,3 +242,3 @@ Extractor.prototype.report = function (node, message) { | ||
Extractor.prototype.unionTypeAliasDeclaration = function (node, tag) { | ||
var e_2, _a; | ||
var e_3, _a; | ||
var name = this.entityName(node, tag); | ||
@@ -238,3 +263,3 @@ if (name == null) | ||
} | ||
catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
catch (e_3_1) { e_3 = { error: e_3_1 }; } | ||
finally { | ||
@@ -244,3 +269,3 @@ try { | ||
} | ||
finally { if (e_2) throw e_2.error; } | ||
finally { if (e_3) throw e_3.error; } | ||
} | ||
@@ -262,3 +287,3 @@ this.ctx.recordTypeName(node.name, name.value); | ||
if (typeParam == null) { | ||
return this.report(funcName, "Expected type extension function to have a first argument representing the type to extend."); | ||
return this.report(funcName, "Expected `@".concat(FIELD_TAG, "` function to have a first argument representing the type to extend.")); | ||
} | ||
@@ -284,3 +309,3 @@ var typeName = this.typeReferenceFromParam(typeParam); | ||
if (!ts.isSourceFile(node.parent)) { | ||
return this.report(node, "Expected type extension function to be a top-level declaration."); | ||
return this.report(node, "Expected `@".concat(FIELD_TAG, "` function to be a top-level declaration.")); | ||
} | ||
@@ -316,6 +341,6 @@ // TODO: Does this work in the browser? | ||
if (typeParam.type == null) { | ||
return this.report(typeParam, "Expected first argument of a type extension function to have an explicit type annotation."); | ||
return this.report(typeParam, "Expected first argument of a `@".concat(FIELD_TAG, "` 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."); | ||
return this.report(typeParam.type, "Expected first argument of a `@".concat(FIELD_TAG, "` function to be typed as a `@gqlType` type.")); | ||
} | ||
@@ -330,3 +355,3 @@ var nameNode = typeParam.type.typeName; | ||
if (node.name == null) { | ||
return this.report(node, "Expected type extension function to be a named function."); | ||
return this.report(node, "Expected a `@".concat(FIELD_TAG, "` function to be a named function.")); | ||
} | ||
@@ -341,6 +366,6 @@ var exportKeyword = (_a = node.modifiers) === null || _a === void 0 ? void 0 : _a.some(function (modifier) { | ||
// TODO: We could support this | ||
return this.report(defaultKeyword, "Expected type extension function to be a named export, not a default export."); | ||
return this.report(defaultKeyword, "Expected a `@".concat(FIELD_TAG, "` 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 this.report(node.name, "Expected a `@".concat(FIELD_TAG, "` function to be a named export.")); | ||
} | ||
@@ -378,3 +403,3 @@ return node.name; | ||
Extractor.prototype.collectInputFields = function (node) { | ||
var e_3, _a; | ||
var e_4, _a; | ||
var fields = []; | ||
@@ -396,3 +421,3 @@ if (!ts.isTypeLiteralNode(node.type)) { | ||
} | ||
catch (e_3_1) { e_3 = { error: e_3_1 }; } | ||
catch (e_4_1) { e_4 = { error: e_4_1 }; } | ||
finally { | ||
@@ -402,3 +427,3 @@ try { | ||
} | ||
finally { if (e_3) throw e_3.error; } | ||
finally { if (e_4) throw e_4.error; } | ||
} | ||
@@ -440,2 +465,3 @@ return fields.length === 0 ? null : fields; | ||
this.ctx.recordTypeName(node.name, name.value); | ||
this.checkForTypenameProperty(node, name.value); | ||
this.definitions.push({ | ||
@@ -459,2 +485,3 @@ kind: graphql_1.Kind.OBJECT_TYPE_DEFINITION, | ||
this.ctx.recordTypeName(node.name, name.value); | ||
this.checkForTypenameProperty(node, name.value); | ||
this.definitions.push({ | ||
@@ -470,2 +497,67 @@ kind: graphql_1.Kind.OBJECT_TYPE_DEFINITION, | ||
}; | ||
Extractor.prototype.checkForTypenameProperty = function (node, expectedName) { | ||
var _this = this; | ||
var hasTypename = node.members.some(function (member) { | ||
return _this.isValidTypeNameProperty(member, expectedName); | ||
}); | ||
if (hasTypename) { | ||
this.ctx.recordHasTypenameField(expectedName); | ||
} | ||
}; | ||
Extractor.prototype.isValidTypeNameProperty = function (member, expectedName) { | ||
if (member.name == null || | ||
!ts.isIdentifier(member.name) || | ||
member.name.text !== "__typename") { | ||
return false; | ||
} | ||
if (ts.isPropertyDeclaration(member)) { | ||
return this.isValidTypenamePropertyDeclaration(member, expectedName); | ||
} | ||
if (ts.isPropertySignature(member)) { | ||
return this.isValidTypenamePropertySignature(member, expectedName); | ||
} | ||
this.report(member.name, | ||
// TODO: Could show what kind we found, but TS AST does not have node names. | ||
"Expected `__typename` to be a property declaration."); | ||
return false; | ||
}; | ||
Extractor.prototype.isValidTypenamePropertyDeclaration = function (node, expectedName) { | ||
// If we have a type annotation, we ask that it be a string literal. | ||
// That means, that if we have one, _and_ it's valid, we're done. | ||
// Otherwise we fall through to the initializer check. | ||
if (node.type != null) { | ||
return this.isValidTypenamePropertyType(node.type, expectedName); | ||
} | ||
if (node.initializer == null) { | ||
this.report(node.name, "Expected `__typename` property to have an initializer or a string literal type. For example: `__typename = \"MyType\"` or `__typename: \"MyType\";`."); | ||
return false; | ||
} | ||
if (!ts.isStringLiteral(node.initializer)) { | ||
this.report(node.initializer, "Expected `__typename` property initializer to be a string literal. For example: `__typename = \"MyType\"` or `__typename: \"MyType\";`."); | ||
return false; | ||
} | ||
if (node.initializer.text !== expectedName) { | ||
this.report(node.initializer, "Expected `__typename` property initializer to be `\"".concat(expectedName, "\"`, found `\"").concat(node.initializer.text, "\"`.")); | ||
return false; | ||
} | ||
return true; | ||
}; | ||
Extractor.prototype.isValidTypenamePropertySignature = function (node, expectedName) { | ||
if (node.type == null) { | ||
this.report(node, "Expected `__typename` property signature to specify the typename as a string literal string type. For example `__typename: \"".concat(expectedName, "\";`")); | ||
return false; | ||
} | ||
return this.isValidTypenamePropertyType(node.type, expectedName); | ||
}; | ||
Extractor.prototype.isValidTypenamePropertyType = function (node, expectedName) { | ||
if (!ts.isLiteralTypeNode(node) || !ts.isStringLiteral(node.literal)) { | ||
this.report(node, "Expected `__typename` property signature to specify the typename as a string literal string type. For example `__typename: \"".concat(expectedName, "\";`")); | ||
return false; | ||
} | ||
if (node.literal.text !== expectedName) { | ||
this.report(node, "Expected `__typename` property to be `\"".concat(expectedName, "\"`")); | ||
return false; | ||
} | ||
return true; | ||
}; | ||
Extractor.prototype.collectInterfaces = function (node) { | ||
@@ -518,3 +610,3 @@ var _this = this; | ||
ts.forEachChild(node, function (node) { | ||
if (ts.isMethodDeclaration(node)) { | ||
if (ts.isMethodDeclaration(node) || ts.isMethodSignature(node)) { | ||
var field = _this.methodDeclaration(node); | ||
@@ -536,3 +628,3 @@ if (field) { | ||
Extractor.prototype.collectArgs = function (argsParam) { | ||
var e_4, _a; | ||
var e_5, _a; | ||
var args = []; | ||
@@ -562,3 +654,3 @@ var argsType = argsParam.type; | ||
} | ||
catch (e_4_1) { e_4 = { error: e_4_1 }; } | ||
catch (e_5_1) { e_5 = { error: e_5_1 }; } | ||
finally { | ||
@@ -568,3 +660,3 @@ try { | ||
} | ||
finally { if (e_4) throw e_4.error; } | ||
finally { if (e_5) throw e_5.error; } | ||
} | ||
@@ -574,3 +666,3 @@ return args; | ||
Extractor.prototype.collectArgDefaults = function (node) { | ||
var e_5, _a; | ||
var e_6, _a; | ||
var defaults = new Map(); | ||
@@ -587,3 +679,3 @@ try { | ||
} | ||
catch (e_5_1) { e_5 = { error: e_5_1 }; } | ||
catch (e_6_1) { e_6 = { error: e_6_1 }; } | ||
finally { | ||
@@ -593,3 +685,3 @@ try { | ||
} | ||
finally { if (e_5) throw e_5.error; } | ||
finally { if (e_6) throw e_6.error; } | ||
} | ||
@@ -663,3 +755,3 @@ return defaults; | ||
Extractor.prototype.enumTypeAliasDeclaration = function (node, tag) { | ||
var e_6, _a; | ||
var e_7, _a; | ||
var name = this.entityName(node, tag); | ||
@@ -693,3 +785,3 @@ if (name == null || name.value == null) { | ||
} | ||
catch (e_6_1) { e_6 = { error: e_6_1 }; } | ||
catch (e_7_1) { e_7 = { error: e_7_1 }; } | ||
finally { | ||
@@ -699,3 +791,3 @@ try { | ||
} | ||
finally { if (e_6) throw e_6.error; } | ||
finally { if (e_7) throw e_7.error; } | ||
} | ||
@@ -712,3 +804,3 @@ this.ctx.recordTypeName(node.name, name.value); | ||
Extractor.prototype.collectEnumValues = function (node) { | ||
var e_7, _a; | ||
var e_8, _a; | ||
var values = []; | ||
@@ -734,3 +826,3 @@ try { | ||
} | ||
catch (e_7_1) { e_7 = { error: e_7_1 }; } | ||
catch (e_8_1) { e_8 = { error: e_8_1 }; } | ||
finally { | ||
@@ -740,3 +832,3 @@ try { | ||
} | ||
finally { if (e_7) throw e_7.error; } | ||
finally { if (e_8) throw e_8.error; } | ||
} | ||
@@ -743,0 +835,0 @@ return values; |
@@ -1,3 +0,3 @@ | ||
import { DocumentNode, GraphQLSchema } from "graphql"; | ||
import { DiagnosticsResult, Result, ReportableDiagnostics } from "./utils/DiagnosticError"; | ||
import { GraphQLSchema } from "graphql"; | ||
import { Result, ReportableDiagnostics } from "./utils/DiagnosticError"; | ||
import * as ts from "typescript"; | ||
@@ -15,2 +15,1 @@ export { applyServerDirectives } from "./serverDirectives"; | ||
export declare function buildSchemaResultWithHost(options: GratsOptions, compilerHost: ts.CompilerHost): Result<GraphQLSchema, ReportableDiagnostics>; | ||
export declare function buildSchemaAst(options: GratsOptions, host: ts.CompilerHost): DiagnosticsResult<DocumentNode>; |
@@ -25,3 +25,3 @@ "use strict"; | ||
exports.__esModule = true; | ||
exports.buildSchemaAst = exports.buildSchemaResultWithHost = exports.buildSchemaResult = exports.applyServerDirectives = void 0; | ||
exports.buildSchemaResultWithHost = exports.buildSchemaResult = exports.applyServerDirectives = void 0; | ||
var graphql_1 = require("graphql"); | ||
@@ -47,36 +47,10 @@ var DiagnosticError_1 = require("./utils/DiagnosticError"); | ||
function buildSchemaResultWithHost(options, compilerHost) { | ||
var docResult = buildSchemaAst(options, compilerHost); | ||
if (docResult.kind === "ERROR") { | ||
return (0, DiagnosticError_1.err)(new DiagnosticError_1.ReportableDiagnostics(compilerHost, docResult.err)); | ||
var schemaResult = extractSchema(options, compilerHost); | ||
if (schemaResult.kind === "ERROR") { | ||
return (0, DiagnosticError_1.err)(new DiagnosticError_1.ReportableDiagnostics(compilerHost, schemaResult.err)); | ||
} | ||
var schema = (0, graphql_1.buildASTSchema)(docResult.value, { assumeValidSDL: true }); | ||
var diagnostics = (0, graphql_1.validateSchema)(schema) | ||
// FIXME: Handle case where query is not defined (no location) | ||
.filter(function (e) { return e.source && e.locations && e.positions; }) | ||
.map(function (e) { return (0, DiagnosticError_1.graphQlErrorToDiagnostic)(e); }); | ||
if (diagnostics.length > 0) { | ||
return (0, DiagnosticError_1.err)(new DiagnosticError_1.ReportableDiagnostics(compilerHost, diagnostics)); | ||
} | ||
return (0, DiagnosticError_1.ok)((0, serverDirectives_1.applyServerDirectives)(schema)); | ||
return (0, DiagnosticError_1.ok)((0, serverDirectives_1.applyServerDirectives)(schemaResult.value)); | ||
} | ||
exports.buildSchemaResultWithHost = buildSchemaResultWithHost; | ||
function buildSchemaAst(options, host) { | ||
var docResult = definitionsFromFile(options, host); | ||
if (docResult.kind === "ERROR") | ||
return docResult; | ||
var doc = docResult.value; | ||
// TODO: Currently this does not detect definitions that shadow builtins | ||
// (`String`, `Int`, etc). However, if we pass a second param (extending an | ||
// existing schema) we do! So, we should find a way to validate that we don't | ||
// shadow builtins. | ||
var validationErrors = (0, validate_1.validateSDL)(doc).map(function (e) { | ||
return (0, DiagnosticError_1.graphQlErrorToDiagnostic)(e); | ||
}); | ||
if (validationErrors.length > 0) { | ||
return (0, DiagnosticError_1.err)(validationErrors); | ||
} | ||
return (0, DiagnosticError_1.ok)(doc); | ||
} | ||
exports.buildSchemaAst = buildSchemaAst; | ||
function definitionsFromFile(options, host) { | ||
function extractSchema(options, host) { | ||
var e_1, _a, e_2, _b; | ||
@@ -91,3 +65,3 @@ var program = ts.createProgram(options.files, options.tsCompilerOptions, host); | ||
// If the file doesn't contain any GraphQL definitions, skip it. | ||
if (!/@GQL/.test(sourceFile.text)) { | ||
if (!/@gql/i.test(sourceFile.text)) { | ||
continue; | ||
@@ -121,3 +95,68 @@ } | ||
} | ||
return ctx.resolveTypes({ kind: graphql_1.Kind.DOCUMENT, definitions: definitions }); | ||
var docResult = ctx.resolveTypes({ kind: graphql_1.Kind.DOCUMENT, definitions: definitions }); | ||
if (docResult.kind === "ERROR") | ||
return docResult; | ||
var doc = docResult.value; | ||
// TODO: Currently this does not detect definitions that shadow builtins | ||
// (`String`, `Int`, etc). However, if we pass a second param (extending an | ||
// existing schema) we do! So, we should find a way to validate that we don't | ||
// shadow builtins. | ||
var validationErrors = (0, validate_1.validateSDL)(doc).map(function (e) { | ||
return (0, DiagnosticError_1.graphQlErrorToDiagnostic)(e); | ||
}); | ||
if (validationErrors.length > 0) { | ||
return (0, DiagnosticError_1.err)(validationErrors); | ||
} | ||
var schema = (0, graphql_1.buildASTSchema)(doc, { assumeValidSDL: true }); | ||
var diagnostics = (0, graphql_1.validateSchema)(schema) | ||
// FIXME: Handle case where query is not defined (no location) | ||
.filter(function (e) { return e.source && e.locations && e.positions; }) | ||
.map(function (e) { return (0, DiagnosticError_1.graphQlErrorToDiagnostic)(e); }); | ||
if (diagnostics.length > 0) { | ||
return (0, DiagnosticError_1.err)(diagnostics); | ||
} | ||
var typenameDiagnostics = validateTypename(schema, ctx); | ||
if (typenameDiagnostics.length > 0) | ||
return (0, DiagnosticError_1.err)(typenameDiagnostics); | ||
return (0, DiagnosticError_1.ok)(schema); | ||
} | ||
function validateTypename(schema, ctx) { | ||
var e_3, _a, e_4, _b; | ||
var _c, _d; | ||
var typenameDiagnostics = []; | ||
var abstractTypes = Object.values(schema.getTypeMap()).filter(graphql_1.isAbstractType); | ||
try { | ||
for (var abstractTypes_1 = __values(abstractTypes), abstractTypes_1_1 = abstractTypes_1.next(); !abstractTypes_1_1.done; abstractTypes_1_1 = abstractTypes_1.next()) { | ||
var type = abstractTypes_1_1.value; | ||
// TODO: If we already implement resolveType, we don't need to check implementors | ||
var typeImplementors = schema.getPossibleTypes(type).filter(graphql_1.isType); | ||
try { | ||
for (var typeImplementors_1 = (e_4 = void 0, __values(typeImplementors)), typeImplementors_1_1 = typeImplementors_1.next(); !typeImplementors_1_1.done; typeImplementors_1_1 = typeImplementors_1.next()) { | ||
var implementor = typeImplementors_1_1.value; | ||
if (!ctx.hasTypename.has(implementor.name)) { | ||
var loc = (_d = (_c = implementor.astNode) === null || _c === void 0 ? void 0 : _c.name) === null || _d === void 0 ? void 0 : _d.loc; | ||
if (loc == null) { | ||
throw new Error("Grats expected the parsed type `".concat(implementor.name, "` to have location information. This is a bug in Grats. Please report it.")); | ||
} | ||
typenameDiagnostics.push((0, DiagnosticError_1.diagnosticAtGraphQLLocation)("Missing __typename on `".concat(implementor.name, "`. The type `").concat(type.name, "` is used in a union or interface, so it must have a `__typename` field."), loc)); | ||
} | ||
} | ||
} | ||
catch (e_4_1) { e_4 = { error: e_4_1 }; } | ||
finally { | ||
try { | ||
if (typeImplementors_1_1 && !typeImplementors_1_1.done && (_b = typeImplementors_1["return"])) _b.call(typeImplementors_1); | ||
} | ||
finally { if (e_4) throw e_4.error; } | ||
} | ||
} | ||
} | ||
catch (e_3_1) { e_3 = { error: e_3_1 }; } | ||
finally { | ||
try { | ||
if (abstractTypes_1_1 && !abstractTypes_1_1.done && (_a = abstractTypes_1["return"])) _a.call(abstractTypes_1); | ||
} | ||
finally { if (e_3) throw e_3.error; } | ||
} | ||
return typenameDiagnostics; | ||
} |
@@ -1,2 +0,2 @@ | ||
import { GraphQLSchema } from "graphql"; | ||
import { DocumentNode, GraphQLSchema } from "graphql"; | ||
export declare const METHOD_NAME_DIRECTIVE = "methodName"; | ||
@@ -7,3 +7,3 @@ export declare const METHOD_NAME_ARG = "name"; | ||
export declare const EXPORTED_FUNCTION_NAME_ARG = "functionName"; | ||
export declare const DIRECTIVES_AST: import("graphql").DocumentNode; | ||
export declare const DIRECTIVES_AST: DocumentNode; | ||
export declare function applyServerDirectives(schema: GraphQLSchema): GraphQLSchema; |
@@ -22,7 +22,10 @@ import { DocumentNode, NameNode } from "graphql"; | ||
_unresolvedTypes: Map<NameNode, ts.Symbol>; | ||
hasTypename: Set<string>; | ||
constructor(checker: ts.TypeChecker, host: ts.CompilerHost); | ||
recordTypeName(node: ts.Node, name: string): void; | ||
recordHasTypenameField(name: string): void; | ||
markUnresolvedType(node: ts.Node, name: NameNode): void; | ||
resolveTypes(doc: DocumentNode): DiagnosticsResult<DocumentNode>; | ||
resolveNamedType(unresolved: NameNode): DiagnosticResult<NameNode>; | ||
validateInterfaceImplementorsHaveTypenameField(): DiagnosticResult<null>; | ||
} |
@@ -35,2 +35,3 @@ "use strict"; | ||
this._unresolvedTypes = new Map(); | ||
this.hasTypename = new Set(); | ||
this.checker = checker; | ||
@@ -51,2 +52,5 @@ this.host = host; | ||
}; | ||
TypeContext.prototype.recordHasTypenameField = function (name) { | ||
this.hasTypename.add(name); | ||
}; | ||
TypeContext.prototype.markUnresolvedType = function (node, name) { | ||
@@ -97,3 +101,3 @@ var symbol = this.checker.getSymbolAtLocation(node); | ||
return (0, DiagnosticError_1.err)({ | ||
messageText: "This type is not a valid GraphQL type. Did you mean to annotate it's definition with `/** @GQLType */` or `/** @GQLScalar */`?", | ||
messageText: "This type is not a valid GraphQL type. Did you mean to annotate it's definition with `/** @gqlType */` or `/** @gqlScalar */`?", | ||
start: unresolved.loc.start, | ||
@@ -108,4 +112,7 @@ length: unresolved.loc.end - unresolved.loc.start, | ||
}; | ||
TypeContext.prototype.validateInterfaceImplementorsHaveTypenameField = function () { | ||
return (0, DiagnosticError_1.ok)(null); | ||
}; | ||
return TypeContext; | ||
}()); | ||
exports.TypeContext = TypeContext; |
@@ -1,4 +0,4 @@ | ||
/** @GQLScalar */ | ||
/** @gqlScalar */ | ||
export type Float = number; | ||
/** @GQLScalar */ | ||
/** @gqlScalar */ | ||
export type Int = number; |
@@ -1,2 +0,2 @@ | ||
import { GraphQLError } from "graphql"; | ||
import { GraphQLError, Location, Source } from "graphql"; | ||
import * as ts from "typescript"; | ||
@@ -25,2 +25,4 @@ type Ok<T> = { | ||
export declare function graphQlErrorToDiagnostic(error: GraphQLError): ts.Diagnostic; | ||
export declare function diagnosticAtGraphQLLocation(message: string, loc: Location): ts.Diagnostic; | ||
export declare function graphqlSourceToSourceFile(source: Source): ts.SourceFile; | ||
export {}; |
"use strict"; | ||
exports.__esModule = true; | ||
exports.graphQlErrorToDiagnostic = exports.FAKE_ERROR_CODE = exports.ReportableDiagnostics = exports.err = exports.ok = void 0; | ||
exports.graphqlSourceToSourceFile = exports.diagnosticAtGraphQLLocation = exports.graphQlErrorToDiagnostic = exports.FAKE_ERROR_CODE = exports.ReportableDiagnostics = exports.err = exports.ok = void 0; | ||
var ts = require("typescript"); | ||
@@ -23,3 +23,3 @@ function ok(value) { | ||
// lets us leverage all of TypeScript's error reporting logic. | ||
return formatted.replace(" TS".concat(exports.FAKE_ERROR_CODE, ": "), ": "); | ||
return formatted.replace(new RegExp(" TS".concat(exports.FAKE_ERROR_CODE, ": "), "g"), ": "); | ||
}; | ||
@@ -49,3 +49,3 @@ ReportableDiagnostics.prototype.formatDiagnosticsWithContext = function () { | ||
if (error.source != null) { | ||
sourceFile = ts.createSourceFile(error.source.name, error.source.body, ts.ScriptTarget.Latest); | ||
sourceFile = graphqlSourceToSourceFile(error.source); | ||
} | ||
@@ -63,1 +63,16 @@ return { | ||
exports.graphQlErrorToDiagnostic = graphQlErrorToDiagnostic; | ||
function diagnosticAtGraphQLLocation(message, loc) { | ||
return { | ||
messageText: message, | ||
file: graphqlSourceToSourceFile(loc.source), | ||
code: exports.FAKE_ERROR_CODE, | ||
category: ts.DiagnosticCategory.Error, | ||
start: loc.start, | ||
length: loc.end - loc.start | ||
}; | ||
} | ||
exports.diagnosticAtGraphQLLocation = diagnosticAtGraphQLLocation; | ||
function graphqlSourceToSourceFile(source) { | ||
return ts.createSourceFile(source.name, source.body, ts.ScriptTarget.Latest); | ||
} | ||
exports.graphqlSourceToSourceFile = graphqlSourceToSourceFile; |
{ | ||
"name": "grats", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"main": "dist/src/index.js", | ||
@@ -5,0 +5,0 @@ "bin": "dist/src/cli.js", |
212
README.md
@@ -7,2 +7,4 @@ # -=[ EXPERIMENTAL PRE ALPHA ]=- | ||
[![Join our Discord!](https://img.shields.io/discord/1089650710796320868?logo=discord)](https://capt.dev/grats-chat) | ||
Grats is a tool for statically infering GraphQL schema from your vanilla | ||
@@ -18,3 +20,4 @@ TypeScript code. | ||
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! | ||
remove the question of mismatches between your implementation and your GraphQL | ||
schema definition. Your implementation _is_ the schema definition! | ||
@@ -24,5 +27,5 @@ ## Example | ||
```ts | ||
/** @GQLType */ | ||
/** @gqlType */ | ||
export default class Query { | ||
/** @GQLField */ | ||
/** @gqlField */ | ||
me(): UserResolver { | ||
@@ -32,3 +35,3 @@ return new UserResolver(); | ||
/** | ||
* @GQLField | ||
* @gqlField | ||
* @deprecated Please use `me` instead. | ||
@@ -43,9 +46,9 @@ */ | ||
* A user in our kick-ass system! | ||
* @GQLType User | ||
* @gqlType User | ||
*/ | ||
class UserResolver { | ||
/** @GQLField */ | ||
/** @gqlField */ | ||
name: string = 'Alice'; | ||
/** @GQLField */ | ||
/** @gqlField */ | ||
greeting(args: { salutation: string }): string { | ||
@@ -72,3 +75,3 @@ return `${args.salutation}, ${this.name}`; | ||
**Give it a try in the [online playground](https://capt.dev/grats-example)!** | ||
**Give it a try in the [online playground](https://capt.dev/grats-sandbox)!** | ||
@@ -93,5 +96,5 @@ ## Quick Start | ||
/** @GQLType */ | ||
/** @gqlType */ | ||
class Query { | ||
/** @GQLField */ | ||
/** @gqlField */ | ||
hello(): string { | ||
@@ -118,2 +121,4 @@ return "Hello world!"; | ||
Try it out on [CodeSandbox](https://capt.dev/grats-sandbox)! | ||
## Configuration | ||
@@ -124,3 +129,3 @@ | ||
```json | ||
```json5 | ||
{ | ||
@@ -143,3 +148,3 @@ "grats": { | ||
TypeScript structures should be included in the schema by marking them with | ||
special JSDoc tags such as `/** @GQLType */` or `/** @GQLField */`. | ||
special JSDoc tags such as `/** @gqlType */` or `/** @gqlField */`. | ||
@@ -153,16 +158,14 @@ Any comment text preceding the JSDoc `@` tag will be used as that element's description. | ||
* [`@GQLType`](#GQLtype) | ||
* [`@GQLInterface`](#GQLinterface) | ||
* [`@GQLField`](#GQLfield) | ||
* [`@GQLUnion`](#GQLunion) | ||
* [`@GQLScalar`](#GQLscalar) | ||
* [`@GQLEnum`](#GQLenum) | ||
* [`@GQLInput`](#GQLinput) | ||
* [`@GQLExtendType`](#GQLExtendType) | ||
* [`@gqlType`](#GQLtype) | ||
* [`@gqlInterface`](#GQLinterface) | ||
* [`@gqlField`](#GQLfield) | ||
* [`@gqlUnion`](#GQLunion) | ||
* [`@gqlScalar`](#GQLscalar) | ||
* [`@gqlEnum`](#GQLenum) | ||
* [`@gqlInput`](#GQLinput) | ||
### @gqlType | ||
### @GQLType | ||
GraphQL types can be defined by placing a `@gqlType` docblock directly before a: | ||
GraphQL types can be defined by placing a `@GQLType` docblock directly before a: | ||
* Class declaration | ||
@@ -174,6 +177,6 @@ * Interface declaration | ||
* 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 class name> | ||
* @gqlType <optional name of the type, if different from class name> | ||
*/ | ||
class MyClass { | ||
/** @GQLField */ | ||
/** @gqlField */ | ||
someField: string; | ||
@@ -186,6 +189,6 @@ } | ||
* 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> | ||
* @gqlType <optional name of the type, if different from interface name> | ||
*/ | ||
interface MyInterface { | ||
/** @GQLField */ | ||
/** @gqlField */ | ||
someField: string; | ||
@@ -195,6 +198,10 @@ } | ||
### @GQLInterface | ||
Note: If your type implements a GraphQL interface or is a member of a GraphQL | ||
union, Grats will remind you to add a `__typename: "MyType"` property to your | ||
class or interface. | ||
GraphQL interfaces can be defined by placing a `@GQLInterface` docblock directly before an: | ||
### @gqlInterface | ||
GraphQL interfaces can be defined by placing a `@gqlInterface` docblock directly before an: | ||
* Interface declaration | ||
@@ -205,6 +212,6 @@ | ||
* A description of my interface. | ||
* @GQLInterface <optional name of the type, if different from class name> | ||
* @gqlInterface <optional name of the type, if different from class name> | ||
*/ | ||
interface MyClass { | ||
/** @GQLField */ | ||
/** @gqlField */ | ||
someField: string; | ||
@@ -214,12 +221,14 @@ } | ||
All `@GQLType` types which implement the interface in TypeScript will | ||
All `@gqlType` types which implement the interface in TypeScript will | ||
automatically implement it in GraphQL as well. | ||
### @GQLField | ||
### @gqlField | ||
Within a `@GQLType` class, you can define GraphQL fields by placing a `@GQLField` directly before a: | ||
You can define GraphQL fields by placing a `@gqlField` directly before a: | ||
* Method declaration | ||
* Method signature | ||
* Property declaration | ||
* Property signature | ||
* Function declaration (with named export) | ||
@@ -229,3 +238,3 @@ ```ts | ||
* A description of some field. | ||
* @GQLField <optional name of the field, if different from property name> | ||
* @gqlField <optional name of the field, if different from property name> | ||
*/ | ||
@@ -236,3 +245,3 @@ someField: string; | ||
* A description of my field. | ||
* @GQLField <optional name of the field, if different from method name> | ||
* @gqlField <optional name of the field, if different from method name> | ||
*/ | ||
@@ -245,3 +254,5 @@ myField(): string { | ||
**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`. | ||
*best practices](https://graphql.org/learn/best-practices/#nullability). This | ||
*behavior can be changed by setting the config option `nullableByDefault` to | ||
`false`. | ||
@@ -251,3 +262,3 @@ If you wish to define arguments for a field, define your argument types inline: | ||
```ts | ||
/** @GQLField */ | ||
/** @gqlField */ | ||
myField(args: { greeting: string }): string { | ||
@@ -261,3 +272,3 @@ return `${args.greeting} World`; | ||
```ts | ||
/** @GQLField */ | ||
/** @gqlField */ | ||
myField({ greeting = "Hello" }: { greeting: string }): string { | ||
@@ -271,3 +282,3 @@ return `${greeting} World`; | ||
```ts | ||
/** @GQLField */ | ||
/** @gqlField */ | ||
myField(args: { | ||
@@ -285,3 +296,3 @@ /** A description of the greeting argument */ | ||
/** | ||
* @GQLField | ||
* @gqlField | ||
* @deprecated Please use myNewField instead. | ||
@@ -294,6 +305,42 @@ */ | ||
### @GQLUnion | ||
Sometimes you want to add a computed field to a non-class type, or extend base | ||
types like `Query` or `Mutation` from another file. Both of these usecases are | ||
enabled by placing a `@gqlField` before an exported function declaration. | ||
GraphQL unions can be defined by placing a `@GQLUnion` docblock directly before a: | ||
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 | ||
* @gqlField <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! | ||
* @gqlField <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. | ||
### @gqlUnion | ||
GraphQL unions can be defined by placing a `@gqlUnion` docblock directly before a: | ||
* Type alias of a union of object types | ||
@@ -304,3 +351,3 @@ | ||
* A description of my union. | ||
* @GQLUnion <optional name of the union, if different from type name> | ||
* @gqlUnion <optional name of the union, if different from type name> | ||
*/ | ||
@@ -310,5 +357,5 @@ type MyUnion = User | Post; | ||
### @GQLScalar | ||
### @gqlScalar | ||
GraphQL custom sclars can be defined by placing a `@GQLScalar` docblock directly before a: | ||
GraphQL custom sclars can be defined by placing a `@gqlScalar` docblock directly before a: | ||
@@ -320,3 +367,3 @@ * Type alias declaration | ||
* A description of my custom scalar. | ||
* @GQLScalar <optional name of the scalar, if different from type name> | ||
* @gqlScalar <optional name of the scalar, if different from type name> | ||
*/ | ||
@@ -331,5 +378,5 @@ type MyCustomString = string; | ||
/** @GQLType */ | ||
/** @gqlType */ | ||
class Query { | ||
/** @GQLField */ | ||
/** @gqlField */ | ||
round(args: {float: Float}): Int { | ||
@@ -341,5 +388,5 @@ return Math.round(args.float); | ||
### @GQLEnum | ||
### @gqlEnum | ||
GraphQL enums can be defined by placing a `@GQLEnum` docblock directly before a: | ||
GraphQL enums can be defined by placing a `@gqlEnum` docblock directly before a: | ||
@@ -352,7 +399,7 @@ * TypeScript enum declaration | ||
* A description of my enum. | ||
* @GQLEnum <optional name of the enum, if different from type name> | ||
* @gqlEnum <optional name of the enum, if different from type name> | ||
*/ | ||
enum MyEnum { | ||
/** A description of my variant */ | ||
OK = "OK" | ||
OK = "OK", | ||
/** A description of my other variant */ | ||
@@ -369,3 +416,3 @@ ERROR = "ERROR" | ||
```ts | ||
/** @GQLEnum */ | ||
/** @gqlEnum */ | ||
enum MyEnum { | ||
@@ -391,3 +438,3 @@ OK = "OK" | ||
* A description of my enum. | ||
* @GQLEnum <optional name of the enum, if different from type name> | ||
* @gqlEnum <optional name of the enum, if different from type name> | ||
*/ | ||
@@ -397,5 +444,5 @@ type MyEnum = "OK" | "ERROR"; | ||
### @GQLInput | ||
### @gqlInput | ||
GraphQL input types can be defined by placing a `@GQLInput` docblock directly before a: | ||
GraphQL input types can be defined by placing a `@gqlInput` docblock directly before a: | ||
@@ -407,3 +454,3 @@ * Type alias declaration | ||
* Description of my input type | ||
* @GQLInput <optional name of the input, if different from type name> | ||
* @gqlInput <optional name of the input, if different from type name> | ||
*/ | ||
@@ -416,45 +463,2 @@ type MyInput = { | ||
### @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 | ||
@@ -511,5 +515,5 @@ | ||
* @mofeiZ and @alunyov for their Relay hack-week project exploring a similar idea. | ||
* @josephsavona for input on the design of [Relay Resolvers](https://relay.dev/docs/guides/relay-resolvers/) which inspired this project. | ||
* @bradzacher for tips on how to handle TypeScript ASTs. | ||
* [@mofeiZ](https://github.com/mofeiZ) and [@alunyov](https://github/alunyov) for their Relay hack-week project exploring a similar idea. | ||
* [@josephsavona](https://github.com/josephsavona) for input on the design of [Relay Resolvers](https://relay.dev/docs/guides/relay-resolvers/) which inspired this project. | ||
* [@bradzacher](https://github.com/bradzacher) for tips on how to handle TypeScript ASTs. | ||
* Everyone who worked on Meta's Hack GraphQL server, the developer experince of which inspired this project. | ||
@@ -516,0 +520,0 @@ * A number of other projects which seem to have explored similar ideas in the past: |
126563
2419
488