Comparing version 0.0.0-main-7408c383 to 0.0.0-main-76d60d78
@@ -36,3 +36,4 @@ { | ||
"trailingComma": "all" | ||
} | ||
}, | ||
"packageManager": "pnpm@8.1.1" | ||
} |
@@ -6,2 +6,14 @@ import { DefinitionNode, FieldDefinitionNode, InputValueDefinitionNode, ListTypeNode, NamedTypeNode, Location as GraphQLLocation, NameNode, Token, TypeNode, NonNullTypeNode, StringValueNode, ConstValueNode, ConstDirectiveNode, ConstArgumentNode, EnumValueDefinitionNode, ConstObjectFieldNode, ConstObjectValueNode, ConstListValueNode } from "graphql"; | ||
import { ConfigOptions } from "./lib"; | ||
export declare const LIBRARY_IMPORT_NAME = "grats"; | ||
export declare const LIBRARY_NAME = "Grats"; | ||
export declare const ISSUE_URL = "https://github.com/captbaritone/grats/issues"; | ||
export declare const TYPE_TAG = "gqlType"; | ||
export declare const FIELD_TAG = "gqlField"; | ||
export declare const SCALAR_TAG = "gqlScalar"; | ||
export declare const INTERFACE_TAG = "gqlInterface"; | ||
export declare const ENUM_TAG = "gqlEnum"; | ||
export declare const UNION_TAG = "gqlUnion"; | ||
export declare const INPUT_TAG = "gqlInput"; | ||
export declare const KILLS_PARENT_ON_EXCEPTION_TAG = "killsParentOnException"; | ||
export declare const ALL_TAGS: string[]; | ||
type ArgDefaults = Map<string, ts.Expression>; | ||
@@ -33,4 +45,5 @@ /** | ||
/** Error handling and location juggling */ | ||
report(node: ts.Node, message: string): null; | ||
reportUnhandled(node: ts.Node, message: string): null; | ||
report(node: ts.Node, message: string, relatedInformation?: ts.DiagnosticRelatedInformation[]): null; | ||
reportUnhandled(node: ts.Node, message: string, relatedInformation?: ts.DiagnosticRelatedInformation[]): null; | ||
related(node: ts.Node, message: string): ts.DiagnosticRelatedInformation; | ||
diagnosticAnnotatedLocation(node: ts.Node): { | ||
@@ -37,0 +50,0 @@ start: number; |
@@ -13,4 +13,20 @@ "use strict"; | ||
}; | ||
var __read = (this && this.__read) || function (o, n) { | ||
var m = typeof Symbol === "function" && o[Symbol.iterator]; | ||
if (!m) return o; | ||
var i = m.call(o), r, ar = [], e; | ||
try { | ||
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); | ||
} | ||
catch (error) { e = { error: error }; } | ||
finally { | ||
try { | ||
if (r && !r.done && (m = i["return"])) m.call(i); | ||
} | ||
finally { if (e) throw e.error; } | ||
} | ||
return ar; | ||
}; | ||
exports.__esModule = true; | ||
exports.Extractor = void 0; | ||
exports.Extractor = exports.ALL_TAGS = exports.KILLS_PARENT_ON_EXCEPTION_TAG = exports.INPUT_TAG = exports.UNION_TAG = exports.ENUM_TAG = exports.INTERFACE_TAG = exports.SCALAR_TAG = exports.FIELD_TAG = exports.TYPE_TAG = exports.ISSUE_URL = exports.LIBRARY_NAME = exports.LIBRARY_IMPORT_NAME = void 0; | ||
var graphql_1 = require("graphql"); | ||
@@ -21,21 +37,23 @@ var DiagnosticError_1 = require("./utils/DiagnosticError"); | ||
var serverDirectives_1 = require("./serverDirectives"); | ||
var LIBRARY_IMPORT_NAME = "grats"; | ||
var LIBRARY_NAME = "Grats"; | ||
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 KILLS_PARENT_ON_EXCEPTION_TAG = "killsParentOnException"; | ||
var ALL_TAGS = [ | ||
TYPE_TAG, | ||
FIELD_TAG, | ||
SCALAR_TAG, | ||
INTERFACE_TAG, | ||
ENUM_TAG, | ||
UNION_TAG, | ||
INPUT_TAG, | ||
var E = require("./Errors"); | ||
var JSDoc_1 = require("./utils/JSDoc"); | ||
exports.LIBRARY_IMPORT_NAME = "grats"; | ||
exports.LIBRARY_NAME = "Grats"; | ||
exports.ISSUE_URL = "https://github.com/captbaritone/grats/issues"; | ||
exports.TYPE_TAG = "gqlType"; | ||
exports.FIELD_TAG = "gqlField"; | ||
exports.SCALAR_TAG = "gqlScalar"; | ||
exports.INTERFACE_TAG = "gqlInterface"; | ||
exports.ENUM_TAG = "gqlEnum"; | ||
exports.UNION_TAG = "gqlUnion"; | ||
exports.INPUT_TAG = "gqlInput"; | ||
exports.KILLS_PARENT_ON_EXCEPTION_TAG = "killsParentOnException"; | ||
exports.ALL_TAGS = [ | ||
exports.TYPE_TAG, | ||
exports.FIELD_TAG, | ||
exports.SCALAR_TAG, | ||
exports.INTERFACE_TAG, | ||
exports.ENUM_TAG, | ||
exports.UNION_TAG, | ||
exports.INPUT_TAG, | ||
]; | ||
@@ -67,79 +85,70 @@ var DEPRECATED_TAG = "deprecated"; | ||
var _this = this; | ||
ts.forEachChild(this.sourceFile, function (node) { | ||
var e_1, _a, e_2, _b; | ||
try { | ||
for (var _c = __values(ts.getJSDocTags(node)), _d = _c.next(); !_d.done; _d = _c.next()) { | ||
var tag = _d.value; | ||
switch (tag.tagName.text) { | ||
case TYPE_TAG: | ||
_this.extractType(node, tag); | ||
break; | ||
case SCALAR_TAG: | ||
_this.extractScalar(node, tag); | ||
break; | ||
case INTERFACE_TAG: | ||
_this.extractInterface(node, tag); | ||
break; | ||
case ENUM_TAG: | ||
_this.extractEnum(node, tag); | ||
break; | ||
case INPUT_TAG: | ||
_this.extractInput(node, tag); | ||
break; | ||
case UNION_TAG: | ||
_this.extractUnion(node, tag); | ||
break; | ||
case FIELD_TAG: | ||
if (ts.isFunctionDeclaration(node)) { | ||
_this.functionDeclarationExtendType(node, tag); | ||
(0, JSDoc_1.traverseJSDocTags)(this.sourceFile, function (node, tag) { | ||
var e_1, _a; | ||
switch (tag.tagName.text) { | ||
case exports.TYPE_TAG: | ||
_this.extractType(node, tag); | ||
break; | ||
case exports.SCALAR_TAG: | ||
_this.extractScalar(node, tag); | ||
break; | ||
case exports.INTERFACE_TAG: | ||
_this.extractInterface(node, tag); | ||
break; | ||
case exports.ENUM_TAG: | ||
_this.extractEnum(node, tag); | ||
break; | ||
case exports.INPUT_TAG: | ||
_this.extractInput(node, tag); | ||
break; | ||
case exports.UNION_TAG: | ||
_this.extractUnion(node, tag); | ||
break; | ||
case exports.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, E.fieldTagOnWrongNode()); | ||
} | ||
break; | ||
case exports.KILLS_PARENT_ON_EXCEPTION_TAG: { | ||
var hasFieldTag = ts.getJSDocTags(node).some(function (t) { | ||
return t.tagName.text === exports.FIELD_TAG; | ||
}); | ||
if (!hasFieldTag) { | ||
_this.report(tag.tagName, E.killsParentOnExceptionOnWrongNode()); | ||
} | ||
break; | ||
} | ||
default: | ||
{ | ||
var lowerCaseTag = tag.tagName.text.toLowerCase(); | ||
if (lowerCaseTag.startsWith("gql")) { | ||
try { | ||
for (var ALL_TAGS_1 = __values(exports.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, E.wrongCasingForGratsTag(tag.tagName.text, t)); | ||
break; | ||
} | ||
} | ||
} | ||
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; | ||
case KILLS_PARENT_ON_EXCEPTION_TAG: | ||
var hasFieldTag = ts.getJSDocTags(node).some(function (t) { | ||
return t.tagName.text === FIELD_TAG; | ||
}); | ||
if (!hasFieldTag) { | ||
_this.report(tag.tagName, "Unexpected `@".concat(KILLS_PARENT_ON_EXCEPTION_TAG, "`. `@").concat(KILLS_PARENT_ON_EXCEPTION_TAG, "` can only be used in field annotation docblocks. Perhaps you are missing a `@").concat(FIELD_TAG, "` tag?")); | ||
} | ||
break; | ||
default: | ||
var lowerCaseTag = tag.tagName.text.toLowerCase(); | ||
if (lowerCaseTag.startsWith("gql")) { | ||
catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
finally { | ||
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; | ||
} | ||
} | ||
if (ALL_TAGS_1_1 && !ALL_TAGS_1_1.done && (_a = ALL_TAGS_1["return"])) _a.call(ALL_TAGS_1); | ||
} | ||
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(", "), ".")); | ||
finally { if (e_1) throw e_1.error; } | ||
} | ||
break; | ||
_this.report(tag.tagName, E.invalidGratsTag(tag.tagName.text)); | ||
} | ||
} | ||
} | ||
break; | ||
} | ||
catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
finally { | ||
try { | ||
if (_d && !_d.done && (_a = _c["return"])) _a.call(_c); | ||
} | ||
finally { if (e_1) throw e_1.error; } | ||
} | ||
}); | ||
@@ -162,3 +171,3 @@ if (this.errors.length > 0) { | ||
else { | ||
this.report(tag, "`@".concat(TYPE_TAG, "` can only be used on class or interface declarations.")); | ||
this.report(tag, E.invalidTypeTagUsage()); | ||
} | ||
@@ -171,3 +180,3 @@ }; | ||
else { | ||
this.report(tag, "`@".concat(SCALAR_TAG, "` can only be used on type alias declarations.")); | ||
this.report(tag, E.invalidScalarTagUsage()); | ||
} | ||
@@ -180,3 +189,3 @@ }; | ||
else { | ||
this.report(tag, "`@".concat(INTERFACE_TAG, "` can only be used on interface declarations.")); | ||
this.report(tag, E.invalidInterfaceTagUsage()); | ||
} | ||
@@ -192,3 +201,3 @@ }; | ||
else { | ||
this.report(tag, "`@".concat(ENUM_TAG, "` can only be used on enum declarations or TypeScript unions.")); | ||
this.report(tag, E.invalidEnumTagUsage()); | ||
} | ||
@@ -201,3 +210,3 @@ }; | ||
else { | ||
this.report(tag, "`@".concat(INPUT_TAG, "` can only be used on type alias declarations.")); | ||
this.report(tag, E.invalidInputTagUsage()); | ||
} | ||
@@ -210,7 +219,7 @@ }; | ||
else { | ||
this.report(tag, "`@".concat(UNION_TAG, "` can only be used on type alias declarations.")); | ||
this.report(tag, E.invalidUnionTagUsage()); | ||
} | ||
}; | ||
/** Error handling and location juggling */ | ||
Extractor.prototype.report = function (node, message) { | ||
Extractor.prototype.report = function (node, message, relatedInformation) { | ||
var start = node.getStart(); | ||
@@ -224,3 +233,4 @@ var length = node.getEnd() - start; | ||
start: start, | ||
length: length | ||
length: length, | ||
relatedInformation: relatedInformation | ||
}); | ||
@@ -231,8 +241,17 @@ return null; | ||
// Gives the user a path forward if they think we should be able to infer this type. | ||
Extractor.prototype.reportUnhandled = function (node, message) { | ||
var suggestion = "If you think ".concat(LIBRARY_NAME, " should be able to infer this type, please report an issue at ").concat(ISSUE_URL, "."); | ||
Extractor.prototype.reportUnhandled = function (node, message, relatedInformation) { | ||
var suggestion = "If you think ".concat(exports.LIBRARY_NAME, " should be able to infer this type, please report an issue at ").concat(exports.ISSUE_URL, "."); | ||
var completedMessage = "".concat(message, "\n\n").concat(suggestion); | ||
this.report(node, completedMessage); | ||
return null; | ||
return this.report(node, completedMessage, relatedInformation); | ||
}; | ||
Extractor.prototype.related = function (node, message) { | ||
return { | ||
category: ts.DiagnosticCategory.Message, | ||
code: 0, | ||
file: node.getSourceFile(), | ||
start: node.getStart(), | ||
length: node.getWidth(), | ||
messageText: message | ||
}; | ||
}; | ||
Extractor.prototype.diagnosticAnnotatedLocation = function (node) { | ||
@@ -258,3 +277,3 @@ var start = node.getStart(); | ||
Extractor.prototype.unionTypeAliasDeclaration = function (node, tag) { | ||
var e_3, _a; | ||
var e_2, _a; | ||
var name = this.entityName(node, tag); | ||
@@ -264,3 +283,3 @@ if (name == null) | ||
if (!ts.isUnionTypeNode(node.type)) { | ||
return this.report(node, "Expected a TypeScript union. `@".concat(UNION_TAG, "` can only be used on TypeScript unions.")); | ||
return this.report(node, E.expectedUnionTypeNode()); | ||
} | ||
@@ -273,3 +292,3 @@ var description = this.collectDescription(node.name); | ||
if (!ts.isTypeReferenceNode(member)) { | ||
return this.reportUnhandled(member, "Expected `@".concat(UNION_TAG, "` union members to be type references.")); | ||
return this.reportUnhandled(member, E.expectedUnionTypeReference()); | ||
} | ||
@@ -281,3 +300,3 @@ var namedType = this.gqlNamedType(member.typeName, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
} | ||
catch (e_3_1) { e_3 = { error: e_3_1 }; } | ||
catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
finally { | ||
@@ -287,3 +306,3 @@ try { | ||
} | ||
finally { if (e_3) throw e_3.error; } | ||
finally { if (e_2) throw e_2.error; } | ||
} | ||
@@ -305,3 +324,3 @@ this.ctx.recordTypeName(node.name, name.value); | ||
if (typeParam == null) { | ||
return this.report(funcName, "Expected `@".concat(FIELD_TAG, "` function to have a first argument representing the type to extend.")); | ||
return this.report(funcName, E.invalidParentArgForFunctionField()); | ||
} | ||
@@ -315,3 +334,3 @@ var typeName = this.typeReferenceFromParam(typeParam); | ||
if (node.type == null) { | ||
return this.report(funcName, "Expected GraphQL field to have an explicit return type."); | ||
return this.report(funcName, E.invalidReturnTypeForFunctionField()); | ||
} | ||
@@ -328,7 +347,9 @@ var type = this.collectMethodType(node.type); | ||
if (!ts.isSourceFile(node.parent)) { | ||
return this.report(node, "Expected `@".concat(FIELD_TAG, "` function to be a top-level declaration.")); | ||
return this.report(node, E.functionFieldNotTopLevel()); | ||
} | ||
// TODO: Does this work in the browser? | ||
var filename = this.ctx.getDestFilePath(node.parent); | ||
var directives = [this.exportDirective(funcName, filename, funcName.text)]; | ||
var directives = [ | ||
this.exportDirective(funcName, filename, funcName.text), | ||
]; | ||
if (funcName.text !== name.value) { | ||
@@ -360,6 +381,6 @@ directives.push(this.methodNameDirective(funcName, funcName.text)); | ||
if (typeParam.type == null) { | ||
return this.report(typeParam, "Expected first argument of a `@".concat(FIELD_TAG, "` function to have an explicit type annotation.")); | ||
return this.report(typeParam, E.functionFieldParentTypeMissing()); | ||
} | ||
if (!ts.isTypeReferenceNode(typeParam.type)) { | ||
return this.report(typeParam.type, "Expected first argument of a `@".concat(FIELD_TAG, "` function to be typed as a `@").concat(TYPE_TAG, "` type.")); | ||
return this.report(typeParam.type, E.functionFieldParentTypeNotValid()); | ||
} | ||
@@ -374,3 +395,3 @@ var nameNode = typeParam.type.typeName; | ||
if (node.name == null) { | ||
return this.report(node, "Expected a `@".concat(FIELD_TAG, "` function to be a named function.")); | ||
return this.report(node, E.functionFieldNotNamed()); | ||
} | ||
@@ -385,6 +406,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 a `@".concat(FIELD_TAG, "` function to be a named export, not a default export.")); | ||
return this.report(defaultKeyword, E.functionFieldDefaultExport()); | ||
} | ||
if (exportKeyword == null) { | ||
return this.report(node.name, "Expected a `@".concat(FIELD_TAG, "` function to be a named export.")); | ||
return this.report(node.name, E.functionFieldNotNamedExport()); | ||
} | ||
@@ -413,2 +434,3 @@ return node.name; | ||
var fields = this.collectInputFields(node); | ||
var deprecatedDirective = this.collectDeprecated(node); | ||
this.definitions.push({ | ||
@@ -419,10 +441,11 @@ kind: graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION, | ||
name: name, | ||
fields: fields !== null && fields !== void 0 ? fields : undefined | ||
fields: fields !== null && fields !== void 0 ? fields : undefined, | ||
directives: deprecatedDirective == null ? undefined : [deprecatedDirective] | ||
}); | ||
}; | ||
Extractor.prototype.collectInputFields = function (node) { | ||
var e_4, _a; | ||
var e_3, _a; | ||
var fields = []; | ||
if (!ts.isTypeLiteralNode(node.type)) { | ||
return this.reportUnhandled(node, "`@".concat(INPUT_TAG, "` can only be used on type literals.")); | ||
return this.reportUnhandled(node, E.inputTypeNotLiteral()); | ||
} | ||
@@ -433,3 +456,3 @@ try { | ||
if (!ts.isPropertySignature(member)) { | ||
this.reportUnhandled(member, "`@".concat(INPUT_TAG, "` types only support property signature members.")); | ||
this.reportUnhandled(member, E.inputTypeFieldNotProperty()); | ||
continue; | ||
@@ -442,3 +465,3 @@ } | ||
} | ||
catch (e_4_1) { e_4 = { error: e_4_1 }; } | ||
catch (e_3_1) { e_3 = { error: e_3_1 }; } | ||
finally { | ||
@@ -448,3 +471,3 @@ try { | ||
} | ||
finally { if (e_4) throw e_4.error; } | ||
finally { if (e_3) throw e_3.error; } | ||
} | ||
@@ -458,3 +481,3 @@ return fields.length === 0 ? null : fields; | ||
if (node.type == null) { | ||
return this.report(node, "Input field must have a type annotation."); | ||
return this.report(node, E.inputFieldUntyped()); | ||
} | ||
@@ -466,2 +489,3 @@ var inner = this.collectType(node.type); | ||
var description = this.collectDescription(node.name); | ||
var deprecatedDirective = this.collectDeprecated(node); | ||
return { | ||
@@ -474,3 +498,3 @@ kind: graphql_1.Kind.INPUT_VALUE_DEFINITION, | ||
defaultValue: undefined, | ||
directives: undefined | ||
directives: deprecatedDirective == null ? undefined : [deprecatedDirective] | ||
}; | ||
@@ -480,3 +504,3 @@ }; | ||
if (node.name == null) { | ||
return this.report(node, "Unexpected `@".concat(TYPE_TAG, "` annotation on unnamed class declaration.")); | ||
return this.report(node, E.typeTagOnUnamedClass()); | ||
} | ||
@@ -525,3 +549,3 @@ var name = this.entityName(node, tag); | ||
if (!ts.isTypeLiteralNode(node.type)) { | ||
this.reportUnhandled(node.type, "Expected `@".concat(TYPE_TAG, "` type to be a type literal. For example: `type Foo = { bar: string }`")); | ||
this.reportUnhandled(node.type, E.typeTagOnAliasOfNonObject()); | ||
return; | ||
@@ -566,5 +590,4 @@ } | ||
} | ||
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."); | ||
this.report(member.name, E.typeNameNotDeclaration()); | ||
return false; | ||
@@ -580,11 +603,11 @@ }; | ||
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\";`."); | ||
this.report(node.name, E.typeNameMissingInitializer()); | ||
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\";`."); | ||
this.report(node.initializer, E.typeNameInitializeNotString()); | ||
return false; | ||
} | ||
if (node.initializer.text !== expectedName) { | ||
this.report(node.initializer, "Expected `__typename` property initializer to be `\"".concat(expectedName, "\"`, found `\"").concat(node.initializer.text, "\"`.")); | ||
this.report(node.initializer, E.typeNameInitializerWrong(expectedName, node.initializer.text)); | ||
return false; | ||
@@ -596,3 +619,3 @@ } | ||
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, "\";`")); | ||
this.report(node, E.typeNameMissingTypeAnnotation(expectedName)); | ||
return false; | ||
@@ -604,7 +627,7 @@ } | ||
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, "\";`")); | ||
this.report(node, E.typeNameTypeNotStringLiteral(expectedName)); | ||
return false; | ||
} | ||
if (node.literal.text !== expectedName) { | ||
this.report(node, "Expected `__typename` property to be `\"".concat(expectedName, "\"`")); | ||
this.report(node, E.typeNameDoesNotMatchExpected(expectedName)); | ||
return false; | ||
@@ -678,7 +701,7 @@ } | ||
Extractor.prototype.collectArgs = function (argsParam) { | ||
var e_5, _a; | ||
var e_4, _a; | ||
var args = []; | ||
var argsType = argsParam.type; | ||
if (argsType == null) { | ||
return this.report(argsParam, "Expected GraphQL field arguments to have a TypeScript type. If there are no arguments, you can use `args: never`."); | ||
return this.report(argsParam, E.argumentParamIsMissingType()); | ||
} | ||
@@ -689,3 +712,3 @@ if (argsType.kind === ts.SyntaxKind.NeverKeyword) { | ||
if (!ts.isTypeLiteralNode(argsType)) { | ||
return this.report(argsType, "Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`."); | ||
return this.report(argsType, E.argumentParamIsNotObject()); | ||
} | ||
@@ -705,3 +728,3 @@ var defaults = null; | ||
} | ||
catch (e_5_1) { e_5 = { error: e_5_1 }; } | ||
catch (e_4_1) { e_4 = { error: e_4_1 }; } | ||
finally { | ||
@@ -711,3 +734,3 @@ try { | ||
} | ||
finally { if (e_5) throw e_5.error; } | ||
finally { if (e_4) throw e_4.error; } | ||
} | ||
@@ -717,3 +740,3 @@ return args; | ||
Extractor.prototype.collectArgDefaults = function (node) { | ||
var e_6, _a; | ||
var e_5, _a; | ||
var defaults = new Map(); | ||
@@ -730,3 +753,3 @@ try { | ||
} | ||
catch (e_6_1) { e_6 = { error: e_6_1 }; } | ||
catch (e_5_1) { e_5 = { error: e_5_1 }; } | ||
finally { | ||
@@ -736,3 +759,3 @@ try { | ||
} | ||
finally { if (e_6) throw e_6.error; } | ||
finally { if (e_5) throw e_5.error; } | ||
} | ||
@@ -764,7 +787,6 @@ return defaults; | ||
} | ||
this.reportUnhandled(node, "Expected GraphQL field argument default values to be a literal."); | ||
return null; | ||
return this.reportUnhandled(node, E.defaultValueIsNotLiteral()); | ||
}; | ||
Extractor.prototype.collectArrayLiteral = function (node) { | ||
var e_7, _a; | ||
var e_6, _a; | ||
var values = []; | ||
@@ -784,3 +806,3 @@ var errors = false; | ||
} | ||
catch (e_7_1) { e_7 = { error: e_7_1 }; } | ||
catch (e_6_1) { e_6 = { error: e_6_1 }; } | ||
finally { | ||
@@ -790,3 +812,3 @@ try { | ||
} | ||
finally { if (e_7) throw e_7.error; } | ||
finally { if (e_6) throw e_6.error; } | ||
} | ||
@@ -803,3 +825,3 @@ if (errors) { | ||
Extractor.prototype.cellectObjectLiteral = function (node) { | ||
var e_8, _a; | ||
var e_7, _a; | ||
var fields = []; | ||
@@ -819,3 +841,3 @@ var errors = false; | ||
} | ||
catch (e_8_1) { e_8 = { error: e_8_1 }; } | ||
catch (e_7_1) { e_7 = { error: e_7_1 }; } | ||
finally { | ||
@@ -825,3 +847,3 @@ try { | ||
} | ||
finally { if (e_8) throw e_8.error; } | ||
finally { if (e_7) throw e_7.error; } | ||
} | ||
@@ -839,6 +861,6 @@ if (errors) { | ||
if (!ts.isPropertyAssignment(node)) { | ||
return this.reportUnhandled(node, "Expected object literal property to be a property assignment."); | ||
return this.reportUnhandled(node, E.defaultArgElementIsNotAssignment()); | ||
} | ||
if (node.name == null) { | ||
return this.reportUnhandled(node, "Expected object literal property to have a name."); | ||
return this.reportUnhandled(node, E.defaultArgPropertyMissingName()); | ||
} | ||
@@ -850,3 +872,3 @@ var name = this.expectIdentifier(node.name); | ||
if (initialize == null) { | ||
return this.report(node, "Expected object literal property to have an initializer. For example: `{ offset = 10}`."); | ||
return this.report(node, E.defaultArgPropertyMissingInitializer()); | ||
} | ||
@@ -866,10 +888,10 @@ var value = this.collectConstValue(initialize); | ||
// TODO: How can I create this error? | ||
return this.report(node, "Expected GraphQL field argument type to be a property signature."); | ||
return this.report(node, E.argIsNotProperty()); | ||
} | ||
if (!ts.isIdentifier(node.name)) { | ||
// TODO: How can I create this error? | ||
return this.report(node.name, "Expected GraphQL field argument names to be a literal."); | ||
return this.report(node.name, E.argNameNotLiteral()); | ||
} | ||
if (node.type == null) { | ||
return this.report(node.name, "Expected GraphQL field argument to have a type."); | ||
return this.report(node.name, E.argNotTyped()); | ||
} | ||
@@ -880,2 +902,8 @@ var type = this.collectType(node.type); | ||
if (node.questionToken) { | ||
/* | ||
// TODO: Don't allow args that are optional but don't accept null | ||
if (type.kind === Kind.NON_NULL_TYPE) { | ||
return this.report(node.questionToken, E.nonNullTypeCannotBeOptional()); | ||
} | ||
*/ | ||
type = this.gqlNullableType(type); | ||
@@ -891,2 +919,3 @@ } | ||
} | ||
var deprecatedDirective = this.collectDeprecated(node); | ||
return { | ||
@@ -899,3 +928,3 @@ kind: graphql_1.Kind.INPUT_VALUE_DEFINITION, | ||
defaultValue: defaultValue || undefined, | ||
directives: [] | ||
directives: deprecatedDirective != null ? [deprecatedDirective] : undefined | ||
}; | ||
@@ -938,3 +967,3 @@ }; | ||
Extractor.prototype.enumTypeAliasVariants = function (node) { | ||
var e_9, _a; | ||
var e_8, _a; | ||
var _b; | ||
@@ -958,3 +987,3 @@ // Semantically we only support deriving enums from type aliases that | ||
if (!ts.isUnionTypeNode(node.type)) { | ||
this.reportUnhandled(node.type, "Expected `@".concat(ENUM_TAG, "` to be a union type, or a string literal in the edge case of a single value enum.")); | ||
this.reportUnhandled(node.type, E.enumTagOnInvalidNode()); | ||
return null; | ||
@@ -971,2 +1000,3 @@ } | ||
if (((_b = symbol === null || symbol === void 0 ? void 0 : symbol.declarations) === null || _b === void 0 ? void 0 : _b.length) === 1) { | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
var declaration = symbol.declarations[0]; | ||
@@ -993,3 +1023,3 @@ if (ts.isTypeAliasDeclaration(declaration)) { | ||
!ts.isStringLiteral(member.literal)) { | ||
this.reportUnhandled(member, "Expected `@".concat(ENUM_TAG, "` enum members to be string literal types. For example: `'foo'`.")); | ||
this.reportUnhandled(member, E.enumVariantNotStringLiteral()); | ||
continue; | ||
@@ -1007,3 +1037,3 @@ } | ||
} | ||
catch (e_9_1) { e_9 = { error: e_9_1 }; } | ||
catch (e_8_1) { e_8 = { error: e_8_1 }; } | ||
finally { | ||
@@ -1013,3 +1043,3 @@ try { | ||
} | ||
finally { if (e_9) throw e_9.error; } | ||
finally { if (e_8) throw e_8.error; } | ||
} | ||
@@ -1019,3 +1049,3 @@ return values; | ||
Extractor.prototype.collectEnumValues = function (node) { | ||
var e_10, _a; | ||
var e_9, _a; | ||
var values = []; | ||
@@ -1027,3 +1057,3 @@ try { | ||
!ts.isStringLiteral(member.initializer)) { | ||
this.reportUnhandled(member, "Expected `@".concat(ENUM_TAG, "` enum members to have string literal initializers. For example: `FOO = 'foo'`.")); | ||
this.reportUnhandled(member, E.enumVariantMissingInitializer()); | ||
continue; | ||
@@ -1042,3 +1072,3 @@ } | ||
} | ||
catch (e_10_1) { e_10 = { error: e_10_1 }; } | ||
catch (e_9_1) { e_9 = { error: e_9_1 }; } | ||
finally { | ||
@@ -1048,3 +1078,3 @@ try { | ||
} | ||
finally { if (e_10) throw e_10.error; } | ||
finally { if (e_9) throw e_9.error; } | ||
} | ||
@@ -1062,3 +1092,3 @@ return values; | ||
if (node.name == null) { | ||
return this.report(node, "Expected GraphQL entity to have a name."); | ||
return this.report(node, E.gqlEntityMissingName()); | ||
} | ||
@@ -1071,3 +1101,3 @@ var id = this.expectIdentifier(node.name); | ||
Extractor.prototype.methodDeclaration = function (node) { | ||
var tag = this.findTag(node, FIELD_TAG); | ||
var tag = this.findTag(node, exports.FIELD_TAG); | ||
if (tag == null) | ||
@@ -1079,3 +1109,3 @@ return null; | ||
if (node.type == null) { | ||
return this.report(node.name, "Expected GraphQL field to have a type."); | ||
return this.report(node.name, E.methodMissingType()); | ||
} | ||
@@ -1133,3 +1163,3 @@ var type = this.collectMethodType(node.type); | ||
if (node.typeArguments == null) { | ||
return this.report(node, "Expected type reference to have type arguments."); | ||
return this.report(node, E.promiseMissingTypeArg()); | ||
} | ||
@@ -1144,3 +1174,3 @@ return node.typeArguments[0]; | ||
if (symbol == null) { | ||
return this.report(node, "Expected TypeScript to be able to resolve this GraphQL entity to a symbol."); | ||
return this.report(node, E.cannotResolveSymbolForDescription()); | ||
} | ||
@@ -1174,4 +1204,4 @@ var doc = symbol.getDocumentationComment(this.ctx.checker); | ||
kind: graphql_1.Kind.DIRECTIVE, | ||
loc: this.loc(tag), | ||
name: this.gqlName(tag, DEPRECATED_TAG), | ||
loc: this.loc(tag.tagName), | ||
name: this.gqlName(tag.tagName, DEPRECATED_TAG), | ||
arguments: args | ||
@@ -1181,3 +1211,3 @@ }; | ||
Extractor.prototype.property = function (node) { | ||
var tag = this.findTag(node, FIELD_TAG); | ||
var tag = this.findTag(node, exports.FIELD_TAG); | ||
if (tag == null) | ||
@@ -1189,3 +1219,3 @@ return null; | ||
if (node.type == null) { | ||
this.report(node.name, "Expected GraphQL field to have a type."); | ||
this.report(node.name, E.propertyFieldMissingType()); | ||
return null; | ||
@@ -1220,2 +1250,4 @@ } | ||
}; | ||
// TODO: Support separate modes for input and output types | ||
// For input nodes and field may only be optional if `null` is a valid value. | ||
Extractor.prototype.collectType = function (node) { | ||
@@ -1237,5 +1269,4 @@ var _this = this; | ||
var types = node.types.filter(function (type) { return !_this.isNullish(type); }); | ||
if (types.length !== 1) { | ||
this.report(node, "Expected exactly one non-nullish type."); | ||
return null; | ||
if (types.length === 0) { | ||
return this.report(node, E.expectedOneNonNullishType()); | ||
} | ||
@@ -1245,2 +1276,11 @@ var type = this.collectType(types[0]); | ||
return null; | ||
if (types.length > 1) { | ||
var _a = __read(types), first = _a[0], rest = _a.slice(1); | ||
// FIXME: If each of `rest` matches `first` this should be okay. | ||
var incompatibleVariants = rest.map(function (tsType) { | ||
return _this.related(tsType, "Other non-nullish type"); | ||
}); | ||
this.report(first, E.expectedOneNonNullishType(), incompatibleVariants); | ||
return null; | ||
} | ||
if (node.types.length > 1) { | ||
@@ -1261,9 +1301,9 @@ return this.gqlNullableType(type); | ||
else if (node.kind === ts.SyntaxKind.NumberKeyword) { | ||
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. e.g. `import { Int, Float } from \"").concat(LIBRARY_IMPORT_NAME, "\";`.")); | ||
return this.report(node, E.ambiguousNumberType()); | ||
} | ||
else if (ts.isTypeLiteralNode(node)) { | ||
return this.report(node, "Unexpected type literal. You may want to define a named GraphQL type elsewhere and reference it here."); | ||
return this.report(node, E.unsupportedTypeLiteral()); | ||
} | ||
// TODO: Better error message. This is okay if it's a type reference, but everything else is not. | ||
this.reportUnhandled(node, "Unknown GraphQL type."); | ||
this.reportUnhandled(node, E.unknownGraphQLType()); | ||
return null; | ||
@@ -1281,3 +1321,3 @@ }; | ||
if (node.typeArguments == null) { | ||
return this.report(node, "Expected type reference to have type arguments."); | ||
return this.report(node, E.pluralTypeMissingParameter()); | ||
} | ||
@@ -1316,3 +1356,3 @@ var element = this.collectType(node.typeArguments[0]); | ||
} | ||
return this.report(node, "Expected an identifier."); | ||
return this.report(node, E.expectedIdentifer()); | ||
}; | ||
@@ -1331,9 +1371,9 @@ Extractor.prototype.findTag = function (node, tagName) { | ||
var tags = ts.getJSDocTags(parentNode); | ||
var killsParentOnExceptions = tags.find(function (tag) { return tag.tagName.text === KILLS_PARENT_ON_EXCEPTION_TAG; }); | ||
var killsParentOnExceptions = tags.find(function (tag) { return tag.tagName.text === exports.KILLS_PARENT_ON_EXCEPTION_TAG; }); | ||
if (killsParentOnExceptions) { | ||
if (!this.configOptions.nullableByDefault) { | ||
this.report(killsParentOnExceptions.tagName, "Unexpected `@".concat(KILLS_PARENT_ON_EXCEPTION_TAG, "` tag. `@").concat(KILLS_PARENT_ON_EXCEPTION_TAG, "` is only supported when the Grats config `nullableByDefault` is enabled.")); | ||
this.report(killsParentOnExceptions.tagName, E.killsParentOnExceptionWithWrongConfig()); | ||
} | ||
if (type.kind !== graphql_1.Kind.NON_NULL_TYPE) { | ||
this.report(killsParentOnExceptions.tagName, "Unexpected `@".concat(KILLS_PARENT_ON_EXCEPTION_TAG, "` tag. `@").concat(KILLS_PARENT_ON_EXCEPTION_TAG, "` is unnessesary on fields that are already nullable.")); | ||
this.report(killsParentOnExceptions.tagName, E.killsParentOnExceptionOnNullable()); | ||
} | ||
@@ -1340,0 +1380,0 @@ return type; |
@@ -29,3 +29,2 @@ "use strict"; | ||
function extractGratsSchemaAtRuntime(runtimeOptions) { | ||
var _a; | ||
var parsedTsConfig = getParsedTsConfig(); | ||
@@ -41,3 +40,3 @@ var schemaResult = (0, lib_1.buildSchemaResult)(parsedTsConfig); | ||
var sdl = (0, utils_1.printSchemaWithDirectives)(runtimeSchema, { assumeValid: true }); | ||
var filePath = (_a = runtimeOptions.emitSchemaFile) !== null && _a !== void 0 ? _a : "./schema.graphql"; | ||
var filePath = runtimeOptions.emitSchemaFile; | ||
fs.writeFileSync(filePath, sdl); | ||
@@ -44,0 +43,0 @@ } |
@@ -65,2 +65,3 @@ "use strict"; | ||
var utils_1 = require("@graphql-tools/utils"); | ||
var graphql_1 = require("graphql"); | ||
function main() { | ||
@@ -114,2 +115,3 @@ return __awaiter(this, void 0, void 0, function () { | ||
var fixturesDir = path.join(__dirname, "fixtures"); | ||
var integrationFixturesDir = path.join(__dirname, "integrationFixtures"); | ||
var testDirs = [ | ||
@@ -146,3 +148,50 @@ { | ||
}, | ||
{ | ||
fixturesDir: integrationFixturesDir, | ||
transformer: function (code, fileName) { return __awaiter(void 0, void 0, void 0, function () { | ||
var filePath, server, options, files, parsedOptions, schemaResult, schema, data; | ||
return __generator(this, function (_a) { | ||
var _b; | ||
switch (_a.label) { | ||
case 0: | ||
filePath = "".concat(integrationFixturesDir, "/").concat(fileName); | ||
return [4 /*yield*/, (_b = filePath, Promise.resolve().then(function () { return require(_b); }))]; | ||
case 1: | ||
server = _a.sent(); | ||
if (server.query == null || typeof server.query !== "string") { | ||
throw new Error("Expected `".concat(filePath, "` to export a query text as `query`")); | ||
} | ||
if (server.Query == null || typeof server.Query !== "function") { | ||
throw new Error("Expected `".concat(filePath, "` to export a Query type as `Query`")); | ||
} | ||
options = { | ||
nullableByDefault: true | ||
}; | ||
files = [filePath, "src/Types.ts"]; | ||
parsedOptions = { | ||
options: {}, | ||
raw: { | ||
grats: options | ||
}, | ||
errors: [], | ||
fileNames: files | ||
}; | ||
schemaResult = (0, lib_1.buildSchemaResult)(parsedOptions); | ||
if (schemaResult.kind === "ERROR") { | ||
throw new Error(schemaResult.err.formatDiagnosticsWithContext()); | ||
} | ||
schema = schemaResult.value; | ||
return [4 /*yield*/, (0, graphql_1.graphql)({ | ||
schema: schema, | ||
source: server.query, | ||
rootValue: new server.Query() | ||
})]; | ||
case 2: | ||
data = _a.sent(); | ||
return [2 /*return*/, JSON.stringify(data, null, 2)]; | ||
} | ||
}); | ||
}); } | ||
}, | ||
]; | ||
main(); |
@@ -74,3 +74,4 @@ "use strict"; | ||
this._testFixtures.push(fileName); | ||
if (filterRegex != null && !fileName.match(filterRegex)) { | ||
var filePath = path.join(fixturesDir, fileName); | ||
if (filterRegex != null && !filePath.match(filterRegex)) { | ||
this._skip.add(fileName); | ||
@@ -77,0 +78,0 @@ } |
"use strict"; | ||
var __read = (this && this.__read) || function (o, n) { | ||
var m = typeof Symbol === "function" && o[Symbol.iterator]; | ||
if (!m) return o; | ||
var i = m.call(o), r, ar = [], e; | ||
try { | ||
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); | ||
} | ||
catch (error) { e = { error: error }; } | ||
finally { | ||
try { | ||
if (r && !r.done && (m = i["return"])) m.call(i); | ||
} | ||
finally { if (e) throw e.error; } | ||
} | ||
return ar; | ||
}; | ||
var __values = (this && this.__values) || function(o) { | ||
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; | ||
if (m) return m.call(o); | ||
if (o && typeof o.length === "number") return { | ||
next: function () { | ||
if (o && i >= o.length) o = void 0; | ||
return { value: o && o[i++], done: !o }; | ||
} | ||
}; | ||
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); | ||
}; | ||
exports.__esModule = true; | ||
@@ -42,2 +69,3 @@ exports.graphqlSourceToSourceFile = exports.diagnosticAtGraphQLLocation = exports.graphQlErrorToDiagnostic = exports.FAKE_ERROR_CODE = exports.ReportableDiagnostics = exports.err = exports.ok = void 0; | ||
function graphQlErrorToDiagnostic(error) { | ||
var e_1, _a; | ||
var position = error.positions[0]; | ||
@@ -47,2 +75,41 @@ if (position == null) { | ||
} | ||
// Start with baseline location infromation | ||
var start = position; | ||
var length = 1; | ||
var relatedInformation; | ||
// Nodes have actual ranges (not just a single position), so we we have one | ||
// (or more!) use that instead. | ||
if (error.nodes != null && error.nodes.length > 0) { | ||
var _b = __read(error.nodes), node = _b[0], rest = _b.slice(1); | ||
if (node.loc != null) { | ||
start = node.loc.start; | ||
length = node.loc.end - node.loc.start; | ||
if (rest.length > 0) { | ||
relatedInformation = []; | ||
try { | ||
for (var rest_1 = __values(rest), rest_1_1 = rest_1.next(); !rest_1_1.done; rest_1_1 = rest_1.next()) { | ||
var relatedNode = rest_1_1.value; | ||
if (relatedNode.loc == null) { | ||
continue; | ||
} | ||
relatedInformation.push({ | ||
category: ts.DiagnosticCategory.Message, | ||
code: exports.FAKE_ERROR_CODE, | ||
messageText: "Related location", | ||
file: graphqlSourceToSourceFile(relatedNode.loc.source), | ||
start: relatedNode.loc.start, | ||
length: relatedNode.loc.end - relatedNode.loc.start | ||
}); | ||
} | ||
} | ||
catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
finally { | ||
try { | ||
if (rest_1_1 && !rest_1_1.done && (_a = rest_1["return"])) _a.call(rest_1); | ||
} | ||
finally { if (e_1) throw e_1.error; } | ||
} | ||
} | ||
} | ||
} | ||
var sourceFile; | ||
@@ -57,5 +124,5 @@ if (error.source != null) { | ||
category: ts.DiagnosticCategory.Error, | ||
start: position, | ||
// FIXME: Improve ranges | ||
length: 1 | ||
start: start, | ||
length: length, | ||
relatedInformation: relatedInformation | ||
}; | ||
@@ -62,0 +129,0 @@ } |
{ | ||
"name": "grats", | ||
"version": "0.0.0-main-7408c383", | ||
"version": "0.0.0-main-76d60d78", | ||
"main": "dist/src/index.js", | ||
@@ -31,2 +31,3 @@ "bin": "dist/src/cli.js", | ||
}, | ||
"packageManager": "pnpm@8.1.1", | ||
"scripts": { | ||
@@ -33,0 +34,0 @@ "test": "ts-node --esm src/tests/test.ts", |
563
README.md
@@ -1,4 +0,4 @@ | ||
# -=[ EXPERIMENTAL PRE ALPHA ]=- | ||
# -=[ ALPHA SOFTWARE ]=- | ||
**This is currently a proof of concept. It won't yet work on any real projects.** | ||
**Grats is still experimental. Feel free to try it out and give feedback, but they api is still in flux** | ||
@@ -9,518 +9,15 @@ # Grats: Implementation-First GraphQL for TypeScript | ||
Grats is a tool for statically infering GraphQL schema from your vanilla | ||
TypeScript code. | ||
**What if building a GraphQL server were as simple as just writing functions?** | ||
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. | ||
When you write your GraphQL server in TypeScript, your fields and resovlers | ||
are _already_ annotated with type information. _Grats leverages your existing | ||
type annotations to automatically extract an executable GraphQL schema from your | ||
generic TypeScript resolver 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! | ||
By making your TypeScript implementation the source of truth, you never have to | ||
worry about valiating that your implementiaton matches your schema. Your | ||
implementation _is_ your schema! | ||
## Examples | ||
## Read the docs: https://grats.capt.dev/ | ||
Grats is flexible enough to work with both class-based and functional | ||
approaches to authoring GraphQL types and resolvers. | ||
### Class-Based | ||
```ts | ||
/** @gqlType */ | ||
export default class Query { | ||
/** @gqlField */ | ||
me(): User { | ||
return new User(); | ||
} | ||
/** | ||
* @gqlField | ||
* @deprecated Please use `me` instead. | ||
*/ | ||
viewer(): User { | ||
return new User(); | ||
} | ||
} | ||
/** | ||
* A user in our kick-ass system! | ||
* @gqlType | ||
*/ | ||
class User { | ||
/** @gqlField */ | ||
name: string = 'Alice'; | ||
/** @gqlField */ | ||
greeting(args: { salutation: string }): string { | ||
return `${args.salutation}, ${this.name}`; | ||
} | ||
} | ||
``` | ||
### Functional | ||
```ts | ||
/** @gqlType */ | ||
export type Query {}; | ||
/** @gqlField */ | ||
export function me(_: Query): User { | ||
return { name: "Alice" }; | ||
} | ||
/** | ||
* @gqlField | ||
* @deprecated Please use `me` instead. | ||
*/ | ||
export function viewer(_: Query): User { | ||
return { name: "Alice" }; | ||
} | ||
/** | ||
* A user in our kick-ass system! | ||
* @gqlType | ||
*/ | ||
type User = { | ||
/** @gqlField */ | ||
name: string; | ||
} | ||
/** @gqlField */ | ||
export function greeting(user: User, args: { salutation: string }): string { | ||
return `${args.salutation}, ${user.name}`; | ||
} | ||
``` | ||
Both of the above examples extract the following GraphQL schema: | ||
```graphql | ||
type Query { | ||
me: User | ||
viewer: User @deprecated(reason: "Please use `me` instead.") | ||
} | ||
"""A user in our kick-ass system!""" | ||
type User { | ||
name: String | ||
greeting(salutation: String!): String | ||
} | ||
``` | ||
**Give it a try in the [online playground](https://capt.dev/grats-sandbox)!** | ||
## Quick Start | ||
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 | ||
npm install express express-graphql grats | ||
``` | ||
**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"); | ||
``` | ||
Try it out on [CodeSandbox](https://capt.dev/grats-sandbox)! | ||
## Configuration | ||
Grats has a few configuration options. They can be set in your project's | ||
`tsconfig.json` file: | ||
```json5 | ||
{ | ||
"grats": { | ||
// Should all fields be typed as nullable in accordance with GraphQL best | ||
// practices? | ||
// https://graphql.org/learn/best-practices/#nullability | ||
// | ||
// Individual fileds can declare themselves as nonnullable by adding the | ||
// docblock tag `@killsParentOnException`. | ||
"nullableByDefault": true, // Default: true | ||
// Should Grats error if it encounters a TypeScript type error? | ||
// Note that Grats will always error if it encounters a TypeScript syntax | ||
// error. | ||
"reportTypeScriptTypeErrors": false, // Default: false | ||
}, | ||
"compilerOptions": { | ||
// ... TypeScript config... | ||
} | ||
} | ||
``` | ||
## API Usage | ||
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 */`. | ||
Any comment text preceding the JSDoc `@` tag will be used as that element's description. | ||
**Note that JSDocs must being with `/**` (two asterix).** However, they may be consolidated into a single line. | ||
The following JSDoc tags are supported: | ||
* [`@gqlType`](#gqltype) | ||
* [`@gqlInterface`](#gqlinterface) | ||
* [`@gqlField`](#gqlfield) | ||
* [`@gqlUnion`](#gqlunion) | ||
* [`@gqlScalar`](#gqlscalar) | ||
* [`@gqlEnum`](#gqlenum) | ||
* [`@gqlInput`](#gqlinput) | ||
Each tag maps directly to a concept in the GraphQL [Schema Definition | ||
Language](https://graphql.org/learn/schema/) (SDL). The documentation below aims | ||
to be complete, but our hope is that you feel empowered to just slap one of | ||
these docblock tags on the relevent TypeScript class/type/method/etc in your | ||
code, and let Grats' helpful error messages guide you. | ||
### @gqlType | ||
GraphQL types can be defined by placing a `@gqlType` docblock directly before a: | ||
* Class declaration | ||
* Interface declaration | ||
* Type alias of a literal type | ||
```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 class name> | ||
*/ | ||
class MyClass { | ||
/** @gqlField */ | ||
someField: string; | ||
} | ||
``` | ||
```ts | ||
/** @gqlType */ | ||
interface MyType { | ||
/** @gqlField */ | ||
someField: string; | ||
} | ||
``` | ||
```ts | ||
/** @gqlType */ | ||
type MyType = { | ||
/** @gqlField */ | ||
someField: string; | ||
} | ||
``` | ||
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. | ||
### @gqlInterface | ||
GraphQL interfaces can be defined by placing a `@gqlInterface` docblock directly before an: | ||
* Interface declaration | ||
```ts | ||
/** | ||
* A description of my interface. | ||
* @gqlInterface <optional name of the type, if different from class name> | ||
*/ | ||
interface MyClass { | ||
/** @gqlField */ | ||
someField: string; | ||
} | ||
``` | ||
All `@gqlType` types which implement the interface in TypeScript will | ||
automatically implement it in GraphQL as well. | ||
**Note**: Types declared using type literals `type MyType = { ... }` cannot yet | ||
implement interfaces. For now, you must use a class declarations for types which | ||
implement interfaces. | ||
### @gqlField | ||
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) | ||
```ts | ||
/** | ||
* A description of some field. | ||
* @gqlField <optional name of the field, if different from property name> | ||
*/ | ||
someField: string; | ||
/** @gqlField */ | ||
myField(): string { | ||
return "Hello World"; | ||
} | ||
``` | ||
#### Field nullability | ||
**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 the config option `nullableByDefault` to | ||
`false`. | ||
With `nullableByDefault` _enabled_, you may declare an individual field as | ||
nonnullable adding the docblock tag `@killsParentOnException`. This will cause | ||
the field to be typed as non-nullable, but _it comes at a price_. Should the | ||
resolver throw, the error will bubble up to the first nullable parent. If | ||
`@killsParentOnException` is used too liberally, small errors can take down huge | ||
portions of your query. | ||
Dissabling `nullableByDefault` is equivilent to marking all nonnullable fields | ||
with `@killsParentOnException`. | ||
```ts | ||
/** | ||
* @gqlField | ||
* @killsParentOnException | ||
*/ | ||
myField(): string { | ||
return this.someOtherMethod(); | ||
} | ||
``` | ||
#### Field arguments | ||
If you wish to define arguments for a field, define your argument types inline: | ||
```ts | ||
/** @gqlField */ | ||
myField(args: { greeting: string }): string { | ||
return `${args.greeting} World`; | ||
} | ||
``` | ||
Default values for arguments can be defined by using the `=` operator with destructuring: | ||
```ts | ||
/** @gqlField */ | ||
myField({ greeting = "Hello" }: { greeting: string }): string { | ||
return `${greeting} World`; | ||
} | ||
``` | ||
```ts | ||
/** @gqlField */ | ||
myField({ greeting = { salutation: "Sup" } }: { greeting: GreetingConfig }): string { | ||
return `${greeting.salutation} World`; | ||
} | ||
``` | ||
#### Field descriptions | ||
Arguments can be given descriptions by using the `/**` syntax: | ||
```ts | ||
/** @gqlField */ | ||
myField(args: { | ||
/** A description of the greeting argument */ | ||
greeting: string | ||
}): string { | ||
return `${args.greeting} World`; | ||
} | ||
``` | ||
#### Deprecated fields | ||
To mark a field as deprecated, use the `@deprecated` JSDoc tag: | ||
```ts | ||
/** | ||
* @gqlField | ||
* @deprecated Please use myNewField instead. | ||
*/ | ||
myOldField(): string { | ||
return "Hello World"; | ||
} | ||
``` | ||
#### Functional style fields | ||
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. | ||
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 | ||
/** @gqlField */ | ||
export function userById(_: Query, args: {id: string}): User { | ||
return DB.getUserById(args.id); | ||
} | ||
``` | ||
Extending Mutation: | ||
```ts | ||
/** @gqlField */ | ||
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 | ||
```ts | ||
/** | ||
* A description of my union. | ||
* @gqlUnion <optional name of the union, if different from type name> | ||
*/ | ||
type MyUnion = User | Post; | ||
``` | ||
### @gqlScalar | ||
GraphQL custom sclars can be defined by placing a `@gqlScalar` docblock directly before a: | ||
* Type alias declaration | ||
```ts | ||
/** | ||
* A description of my custom scalar. | ||
* @gqlScalar <optional name of the scalar, if different from type name> | ||
*/ | ||
type MyCustomString = string; | ||
``` | ||
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, ID } from "grats"; | ||
/** @gqlType */ | ||
class Math { | ||
id: ID; | ||
/** @gqlField */ | ||
round(args: {float: Float}): Int { | ||
return Math.round(args.float); | ||
} | ||
} | ||
``` | ||
### @gqlEnum | ||
GraphQL enums can be defined by placing a `@gqlEnum` docblock directly before a: | ||
* TypeScript enum declaration | ||
* Type alias of a union of string literals | ||
```ts | ||
/** | ||
* A description of my enum. | ||
* @gqlEnum <optional name of the enum, if different from type name> | ||
*/ | ||
enum MyEnum { | ||
/** A description of my value */ | ||
OK = "OK", | ||
/** A description of my other value */ | ||
ERROR = "ERROR" | ||
} | ||
``` | ||
Note that the values of the enum are used as the GraphQL enum values, and must | ||
be string literals. | ||
To mark a variants as deprecated, use the `@deprecated` JSDoc tag directly before it: | ||
```ts | ||
/** @gqlEnum */ | ||
enum MyEnum { | ||
OK = "OK" | ||
/** @deprecated Please use OK instead. */ | ||
OKAY = "OKAY" | ||
ERROR = "ERROR" | ||
} | ||
``` | ||
We also support defining enums using a union of string literals, however there | ||
are some limitations to this approach: | ||
* You cannot add descriptions to enum values | ||
* You cannot mark enum values as deprecated | ||
This is due to the fact that TypeScript does not see JSDoc comments as | ||
"attaching" to string literal types. | ||
```ts | ||
/** @gqlEnum */ | ||
type MyEnum = "OK" | "ERROR"; | ||
``` | ||
### @gqlInput | ||
GraphQL input types can be defined by placing a `@gqlInput` docblock directly before a: | ||
* Type alias declaration | ||
```ts | ||
/** | ||
* Description of my input type | ||
* @gqlInput <optional name of the input, if different from type name> | ||
*/ | ||
type MyInput = { | ||
name: string; | ||
age: number; | ||
}; | ||
``` | ||
## Example | ||
See `examples/express-graphql/` in the repo root for a working example. Here we | ||
run the static analysis at startup time. Nice for development, but not ideal for | ||
production where you would want to cache the schema and write it to disk for | ||
other tools to see. | ||
## Contributing | ||
@@ -530,40 +27,2 @@ | ||
# FAQ | ||
## Why would I _not_ want to use Grats | ||
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? | ||
Using decorators to signal that a class/method/etc should be included in the | ||
schema would have some advantages: | ||
* The syntax is well defined, so it: | ||
* Can be checked/documented by TypeScript types | ||
* Formatted with tools like Prettier | ||
* Would not require custom parsing/validaiton rules | ||
However, it also has some disadvantages: | ||
* The feature is technically "experimental" in TypeScript and may change in the future. | ||
* Decorators cannot be applied to types, so it would precude the ability to | ||
define GraphQL constructs using types (e.g. interfaces, unions, etc). | ||
* Decorators cannot be applied to parameters, so it would preclude the ability | ||
to define GraphQL constructs using parameters (e.g. field arguments). | ||
* Decorators are a runtime construct, which means they must be imported and give | ||
the impression that they might have some runtime behavior. This is not the | ||
case for Grats, which is purely a static analysis tool. | ||
Given these tradeoffs, we've decided to use comments instead of decorators. | ||
# Acknowledgements | ||
@@ -570,0 +29,0 @@ |
157432
35
3315
36
6