Comparing version 0.0.23 to 0.0.24
{ | ||
"name": "grats", | ||
"version": "0.0.22", | ||
"version": "0.0.23", | ||
"main": "dist/src/index.js", | ||
@@ -5,0 +5,0 @@ "bin": "dist/src/cli.js", |
@@ -374,4 +374,5 @@ "use strict"; | ||
} | ||
var metadata = (0, metadataDirectives_1.parseFieldMetadataDirective)(metadataDirective); | ||
if (!metadata.asyncIterable) { | ||
// Note: We assume the default name is used here. When custom operation types are supported | ||
// we'll need to update this. | ||
if (parentTypeName !== "Subscription") { | ||
var resolve = this.resolveMethod(field, "resolve", parentTypeName); | ||
@@ -378,0 +379,0 @@ return [this.maybeApplySemanticNullRuntimeCheck(field, resolve)]; |
@@ -56,2 +56,3 @@ export declare const ISSUE_URL = "https://github.com/captbaritone/grats/issues"; | ||
export declare function wrapperMissingTypeArg(): string; | ||
export declare function invalidWrapperOnInputType(wrapperName: string): string; | ||
export declare function cannotResolveSymbolForDescription(): string; | ||
@@ -92,3 +93,2 @@ export declare function propertyFieldMissingType(): string; | ||
export declare function subscriptionFieldNotAsyncIterable(): string; | ||
export declare function nonSubscriptionFieldAsyncIterable(): string; | ||
export declare function operationTypeNotUnknown(): string; | ||
@@ -99,1 +99,5 @@ export declare function expectedNullableArgumentToBeOptional(): string; | ||
export declare function gqlTagInDetachedJSDocBlockComment(): string; | ||
export declare function gqlFieldTagOnInputType(): string; | ||
export declare function gqlFieldParentMissingTag(): string; | ||
export declare function missingSpecifiedByUrl(): string; | ||
export declare function specifiedByOnWrongNode(): string; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.defaultValueIsNotLiteral = exports.ambiguousNumberType = exports.expectedOneNonNullishType = exports.propertyFieldMissingType = exports.cannotResolveSymbolForDescription = exports.wrapperMissingTypeArg = exports.methodMissingType = exports.gqlEntityMissingName = exports.enumVariantMissingInitializer = exports.enumVariantNotStringLiteral = exports.enumTagOnInvalidNode = exports.argNotTyped = exports.argNameNotLiteral = exports.argIsNotProperty = exports.argumentParamIsNotObject = exports.argumentParamIsMissingType = exports.typeNameDoesNotMatchExpected = exports.typeNameTypeNotStringLiteral = exports.typeNameMissingTypeAnnotation = exports.typeNameInitializerWrong = exports.typeNameInitializeNotString = exports.typeNameMissingInitializer = exports.typeNameNotDeclaration = exports.typeTagOnAliasOfNonObjectOrUnknown = exports.typeTagOnUnnamedClass = exports.inputFieldUntyped = exports.inputInterfaceFieldNotProperty = exports.inputTypeFieldNotProperty = exports.inputTypeNotLiteral = exports.functionFieldNotNamedExport = exports.functionFieldDefaultExport = exports.functionFieldNotNamed = exports.functionFieldParentTypeNotValid = exports.functionFieldParentTypeMissing = exports.functionFieldNotTopLevel = exports.invalidReturnTypeForFunctionField = exports.invalidParentArgForFunctionField = exports.expectedUnionTypeReference = exports.expectedUnionTypeNode = exports.invalidUnionTagUsage = exports.invalidInputTagUsage = exports.invalidEnumTagUsage = exports.invalidInterfaceTagUsage = exports.invalidScalarTagUsage = exports.invalidTypeTagUsage = exports.invalidGratsTag = exports.wrongCasingForGratsTag = exports.killsParentOnExceptionOnWrongNode = exports.fieldTagOnWrongNode = exports.ISSUE_URL = void 0; | ||
exports.gqlTagInDetachedJSDocBlockComment = exports.gqlTagInNonJSDocBlockComment = exports.gqlTagInLineComment = exports.expectedNullableArgumentToBeOptional = exports.operationTypeNotUnknown = exports.nonSubscriptionFieldAsyncIterable = exports.subscriptionFieldNotAsyncIterable = exports.graphQLTagNameHasWhitespace = exports.graphQLNameHasLeadingNewlines = exports.multipleContextTypes = exports.unexpectedParamSpreadForContextParam = exports.expectedTypeAnnotationOnContextToHaveDeclaration = exports.expectedTypeAnnotationOnContextToBeResolvable = exports.expectedTypeAnnotationOfReferenceOnContext = exports.expectedTypeAnnotationOnContext = exports.unresolvedTypeReference = exports.invalidTypePassedToFieldFunction = exports.parameterPropertyMissingType = exports.parameterPropertyNotPublic = exports.parameterWithoutModifiers = exports.duplicateInterfaceTag = exports.duplicateTag = exports.implementsTagOnTypeAlias = exports.implementsTagOnInterface = exports.implementsTagOnClass = exports.mergedInterfaces = exports.nonNullTypeCannotBeOptional = exports.killsParentOnExceptionOnNullable = exports.killsParentOnExceptionWithWrongConfig = exports.expectedNameIdentifier = exports.pluralTypeMissingParameter = exports.unknownGraphQLType = exports.unsupportedTypeLiteral = exports.defaultArgPropertyMissingInitializer = exports.defaultArgPropertyMissingName = exports.defaultArgElementIsNotAssignment = void 0; | ||
exports.ambiguousNumberType = exports.expectedOneNonNullishType = exports.propertyFieldMissingType = exports.cannotResolveSymbolForDescription = exports.invalidWrapperOnInputType = exports.wrapperMissingTypeArg = exports.methodMissingType = exports.gqlEntityMissingName = exports.enumVariantMissingInitializer = exports.enumVariantNotStringLiteral = exports.enumTagOnInvalidNode = exports.argNotTyped = exports.argNameNotLiteral = exports.argIsNotProperty = exports.argumentParamIsNotObject = exports.argumentParamIsMissingType = exports.typeNameDoesNotMatchExpected = exports.typeNameTypeNotStringLiteral = exports.typeNameMissingTypeAnnotation = exports.typeNameInitializerWrong = exports.typeNameInitializeNotString = exports.typeNameMissingInitializer = exports.typeNameNotDeclaration = exports.typeTagOnAliasOfNonObjectOrUnknown = exports.typeTagOnUnnamedClass = exports.inputFieldUntyped = exports.inputInterfaceFieldNotProperty = exports.inputTypeFieldNotProperty = exports.inputTypeNotLiteral = exports.functionFieldNotNamedExport = exports.functionFieldDefaultExport = exports.functionFieldNotNamed = exports.functionFieldParentTypeNotValid = exports.functionFieldParentTypeMissing = exports.functionFieldNotTopLevel = exports.invalidReturnTypeForFunctionField = exports.invalidParentArgForFunctionField = exports.expectedUnionTypeReference = exports.expectedUnionTypeNode = exports.invalidUnionTagUsage = exports.invalidInputTagUsage = exports.invalidEnumTagUsage = exports.invalidInterfaceTagUsage = exports.invalidScalarTagUsage = exports.invalidTypeTagUsage = exports.invalidGratsTag = exports.wrongCasingForGratsTag = exports.killsParentOnExceptionOnWrongNode = exports.fieldTagOnWrongNode = exports.ISSUE_URL = void 0; | ||
exports.specifiedByOnWrongNode = exports.missingSpecifiedByUrl = exports.gqlFieldParentMissingTag = exports.gqlFieldTagOnInputType = exports.gqlTagInDetachedJSDocBlockComment = exports.gqlTagInNonJSDocBlockComment = exports.gqlTagInLineComment = exports.expectedNullableArgumentToBeOptional = exports.operationTypeNotUnknown = exports.subscriptionFieldNotAsyncIterable = exports.graphQLTagNameHasWhitespace = exports.graphQLNameHasLeadingNewlines = exports.multipleContextTypes = exports.unexpectedParamSpreadForContextParam = exports.expectedTypeAnnotationOnContextToHaveDeclaration = exports.expectedTypeAnnotationOnContextToBeResolvable = exports.expectedTypeAnnotationOfReferenceOnContext = exports.expectedTypeAnnotationOnContext = exports.unresolvedTypeReference = exports.invalidTypePassedToFieldFunction = exports.parameterPropertyMissingType = exports.parameterPropertyNotPublic = exports.parameterWithoutModifiers = exports.duplicateInterfaceTag = exports.duplicateTag = exports.implementsTagOnTypeAlias = exports.implementsTagOnInterface = exports.implementsTagOnClass = exports.mergedInterfaces = exports.nonNullTypeCannotBeOptional = exports.killsParentOnExceptionOnNullable = exports.killsParentOnExceptionWithWrongConfig = exports.expectedNameIdentifier = exports.pluralTypeMissingParameter = exports.unknownGraphQLType = exports.unsupportedTypeLiteral = exports.defaultArgPropertyMissingInitializer = exports.defaultArgPropertyMissingName = exports.defaultArgElementIsNotAssignment = exports.defaultValueIsNotLiteral = void 0; | ||
var Extractor_1 = require("./Extractor"); | ||
@@ -24,3 +24,3 @@ exports.ISSUE_URL = "https://github.com/captbaritone/grats/issues"; | ||
function fieldTagOnWrongNode() { | ||
return "`@".concat(Extractor_1.FIELD_TAG, "` can only be used on method/property declarations or signatures."); | ||
return "`@".concat(Extractor_1.FIELD_TAG, "` can only be used on method/property declarations, signatures, or function declarations."); | ||
} | ||
@@ -203,2 +203,6 @@ exports.fieldTagOnWrongNode = fieldTagOnWrongNode; | ||
exports.wrapperMissingTypeArg = wrapperMissingTypeArg; | ||
function invalidWrapperOnInputType(wrapperName) { | ||
return "Invalid input type. `".concat(wrapperName, "` is not a valid type when used as a GraphQL input value."); | ||
} | ||
exports.invalidWrapperOnInputType = invalidWrapperOnInputType; | ||
function cannotResolveSymbolForDescription() { | ||
@@ -358,6 +362,2 @@ return "Expected TypeScript to be able to resolve this GraphQL entity to a symbol. Is it possible that this type is not defined in this file? Grats needs to follow type references to their declaration in order to determine which GraphQL name is being referenced."; | ||
exports.subscriptionFieldNotAsyncIterable = subscriptionFieldNotAsyncIterable; | ||
function nonSubscriptionFieldAsyncIterable() { | ||
return "Unexpected AsyncIterable. Only fields on `Subscription` should return an `AsyncIterable`. Non-subscription fields are only expected to return a single value."; | ||
} | ||
exports.nonSubscriptionFieldAsyncIterable = nonSubscriptionFieldAsyncIterable; | ||
function operationTypeNotUnknown() { | ||
@@ -383,1 +383,17 @@ return "Operation types `Query`, `Mutation`, and `Subscription` must be defined as type aliases of `unknown`. E.g. `type Query = unknown`. This is because GraphQL servers do not have an agreed upon way to produce root values, and Grats errs on the side of safety. If you are trying to implement dependency injection, consider using the `context` argument passed to each resolver instead. If you have a strong use case for a concrete root value, please file an issue."; | ||
exports.gqlTagInDetachedJSDocBlockComment = gqlTagInDetachedJSDocBlockComment; | ||
function gqlFieldTagOnInputType() { | ||
return "The tag `@".concat(Extractor_1.FIELD_TAG, "` is not needed on fields of input types. All fields are automatically included as part of the input type. This tag can be safely removed."); | ||
} | ||
exports.gqlFieldTagOnInputType = gqlFieldTagOnInputType; | ||
function gqlFieldParentMissingTag() { | ||
return "Unexpected `@".concat(Extractor_1.FIELD_TAG, "`. The parent construct must be either a `@").concat(Extractor_1.TYPE_TAG, "` or `@").concat(Extractor_1.INTERFACE_TAG, "` tag. Are you missing one of these tags?"); | ||
} | ||
exports.gqlFieldParentMissingTag = gqlFieldParentMissingTag; | ||
function missingSpecifiedByUrl() { | ||
return "Expected `@".concat(Extractor_1.SPECIFIED_BY_TAG, "` tag to be followed by a URL. This URL will be used as the `url` argument to the `@specifiedBy` directive in the generated GraphQL schema. See https://spec.graphql.org/draft/#sec--specifiedBy for more information."); | ||
} | ||
exports.missingSpecifiedByUrl = missingSpecifiedByUrl; | ||
function specifiedByOnWrongNode() { | ||
return "Unexpected `@".concat(Extractor_1.SPECIFIED_BY_TAG, "` tag on non-scalar declaration. `@").concat(Extractor_1.SPECIFIED_BY_TAG, "` can only be used on custom scalar declarations. Are you missing a `@").concat(Extractor_1.SCALAR_TAG, "` tag?"); | ||
} | ||
exports.specifiedByOnWrongNode = specifiedByOnWrongNode; |
@@ -18,2 +18,3 @@ import { NameNode } from "graphql"; | ||
export declare const ALL_TAGS: string[]; | ||
export declare const SPECIFIED_BY_TAG = "specifiedBy"; | ||
export type ExtractionSnapshot = { | ||
@@ -20,0 +21,0 @@ readonly definitions: GratsDefinitionNode[]; |
@@ -30,3 +30,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.extract = exports.ALL_TAGS = exports.KILLS_PARENT_ON_EXCEPTION_TAG = exports.IMPLEMENTS_TAG_DEPRECATED = exports.INPUT_TAG = exports.UNION_TAG = exports.ENUM_TAG = exports.INTERFACE_TAG = exports.SCALAR_TAG = exports.FIELD_TAG = exports.TYPE_TAG = exports.LIBRARY_NAME = exports.LIBRARY_IMPORT_NAME = void 0; | ||
exports.extract = exports.SPECIFIED_BY_TAG = exports.ALL_TAGS = exports.KILLS_PARENT_ON_EXCEPTION_TAG = exports.IMPLEMENTS_TAG_DEPRECATED = exports.INPUT_TAG = exports.UNION_TAG = exports.ENUM_TAG = exports.INTERFACE_TAG = exports.SCALAR_TAG = exports.FIELD_TAG = exports.TYPE_TAG = exports.LIBRARY_NAME = exports.LIBRARY_IMPORT_NAME = void 0; | ||
var graphql_1 = require("graphql"); | ||
@@ -66,2 +66,3 @@ var DiagnosticError_1 = require("./utils/DiagnosticError"); | ||
var DEPRECATED_TAG = "deprecated"; | ||
exports.SPECIFIED_BY_TAG = "specifiedBy"; | ||
var OPERATION_TYPES = new Set(["Query", "Mutation", "Subscription"]); | ||
@@ -134,18 +135,29 @@ /** | ||
} | ||
else if (!(ts.isParameter(node) || | ||
ts.isMethodDeclaration(node) || | ||
ts.isGetAccessorDeclaration(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, "field", E.fieldTagOnWrongNode()); | ||
else { | ||
// Non-function fields must be defined as a decent of something that | ||
// is annotated with @gqlType or @gqlInterface. | ||
// | ||
// The actual field will get extracted when we traverse the parent, but | ||
// we need to report an error if the parent is not a valid type or is not | ||
// annotated with @gqlType or @gqlInterface. Otherwise, the user may get | ||
// confused as to why the field is not showing up in the schema. | ||
var parent = getFieldParent(node); | ||
// If there was no valid parent, report an error. | ||
if (parent === null) { | ||
_this.reportUnhandled(node, "field", E.fieldTagOnWrongNode()); | ||
} | ||
else if (_this.hasTag(parent, exports.INPUT_TAG)) { | ||
// You don't need to add `@gqlField` to input types, but it's an | ||
// easy mistake to think you might need to. We report a helpful | ||
// error in this case. | ||
_this.report(tag, E.gqlFieldTagOnInputType()); | ||
} | ||
else if (!_this.hasTag(parent, exports.TYPE_TAG) && | ||
!_this.hasTag(parent, exports.INTERFACE_TAG)) { | ||
_this.report(tag, E.gqlFieldParentMissingTag()); | ||
} | ||
} | ||
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) { | ||
if (!_this.hasTag(node, exports.FIELD_TAG)) { | ||
_this.report(tag.tagName, E.killsParentOnExceptionOnWrongNode()); | ||
@@ -156,2 +168,8 @@ } | ||
} | ||
case exports.SPECIFIED_BY_TAG: { | ||
if (!_this.hasTag(node, exports.SCALAR_TAG)) { | ||
_this.report(tag.tagName, E.specifiedByOnWrongNode()); | ||
} | ||
break; | ||
} | ||
default: | ||
@@ -318,6 +336,5 @@ { | ||
} | ||
var returnType = this.collectReturnType(node.type); | ||
if (returnType == null) | ||
var type = this.collectType(node.type, { kind: "OUTPUT" }); | ||
if (type == null) | ||
return null; | ||
var type = returnType.type, asyncIterable = returnType.asyncIterable; | ||
var args = null; | ||
@@ -343,3 +360,2 @@ var argsParam = node.parameters[1]; | ||
argCount: node.parameters.length, | ||
asyncIterable: asyncIterable, | ||
}), | ||
@@ -396,3 +412,5 @@ ]; | ||
this.recordTypeName(node.name, name, "SCALAR"); | ||
this.definitions.push(this.gql.scalarTypeDefinition(node, name, description)); | ||
// TODO: Can a scalar be deprecated? | ||
var specifiedByDirective = this.collectSpecifiedBy(node); | ||
this.definitions.push(this.gql.scalarTypeDefinition(node, name, specifiedByDirective == null ? null : [specifiedByDirective], description)); | ||
}; | ||
@@ -474,3 +492,3 @@ Extractor.prototype.inputTypeAliasDeclaration = function (node, tag) { | ||
} | ||
var inner = this.collectType(node.type); | ||
var inner = this.collectType(node.type, { kind: "INPUT" }); | ||
if (inner == null) | ||
@@ -754,6 +772,5 @@ return null; | ||
argCount: null, | ||
asyncIterable: null, | ||
}), | ||
]; | ||
var type = this.collectType(node.type); | ||
var type = this.collectType(node.type, { kind: "OUTPUT" }); | ||
if (type == null) | ||
@@ -942,3 +959,3 @@ return null; | ||
} | ||
var type = this.collectType(node.type); | ||
var type = this.collectType(node.type, { kind: "INPUT" }); | ||
if (type == null) | ||
@@ -1130,6 +1147,5 @@ return null; | ||
} | ||
var returnType = this.collectReturnType(node.type); | ||
if (returnType == null) | ||
var type = this.collectType(node.type, { kind: "OUTPUT" }); | ||
if (type == null) | ||
return null; | ||
var type = returnType.type, asyncIterable = returnType.asyncIterable; | ||
// We already reported an error | ||
@@ -1156,3 +1172,2 @@ if (type == null) | ||
argCount: isCallable(node) ? node.parameters.length : null, | ||
asyncIterable: asyncIterable, | ||
}), | ||
@@ -1170,47 +1185,2 @@ ]; | ||
}; | ||
Extractor.prototype.collectReturnType = function (node) { | ||
if (ts.isTypeReferenceNode(node)) { | ||
var identifier = this.expectNameIdentifier(node.typeName); | ||
if (identifier == null) | ||
return null; | ||
if (identifier.text == "AsyncIterable") { | ||
if (node.typeArguments == null || node.typeArguments.length === 0) { | ||
// TODO: Better error? | ||
return this.report(node, E.wrapperMissingTypeArg()); | ||
} | ||
var t_1 = this.collectType(node.typeArguments[0]); | ||
if (t_1 == null) | ||
return null; | ||
return { type: t_1, asyncIterable: identifier }; | ||
} | ||
} | ||
var inner = this.maybeUnwrapPromise(node); | ||
if (inner == null) | ||
return null; | ||
var t = this.collectType(inner); | ||
if (t == null) | ||
return null; | ||
return { type: t }; | ||
}; | ||
Extractor.prototype.collectPropertyType = function (node) { | ||
// TODO: Handle function types here. | ||
var inner = this.maybeUnwrapPromise(node); | ||
if (inner == null) | ||
return null; | ||
return this.collectType(inner); | ||
}; | ||
Extractor.prototype.maybeUnwrapPromise = function (node) { | ||
if (ts.isTypeReferenceNode(node)) { | ||
var identifier = this.expectNameIdentifier(node.typeName); | ||
if (identifier == null) | ||
return null; | ||
if (identifier.text === "Promise") { | ||
if (node.typeArguments == null || node.typeArguments.length === 0) { | ||
return this.report(node, E.wrapperMissingTypeArg()); | ||
} | ||
return node.typeArguments[0]; | ||
} | ||
} | ||
return node; | ||
}; | ||
Extractor.prototype.collectDescription = function (node) { | ||
@@ -1243,2 +1213,14 @@ var docs = | ||
}; | ||
Extractor.prototype.collectSpecifiedBy = function (node) { | ||
var tag = this.findTag(node, exports.SPECIFIED_BY_TAG); | ||
if (tag == null) | ||
return null; | ||
var urlComment = tag.comment && ts.getTextOfJSDocComment(tag.comment); | ||
if (urlComment == null) { | ||
return this.report(tag, "Expected @specifiedBy tag to be followed by a URL."); | ||
} | ||
// FIXME: Use the _value_'s location not the tag's | ||
var reason = this.gql.constArgument(tag, this.gql.name(tag, "url"), this.gql.string(tag, urlComment)); | ||
return this.gql.constDirective(tag.tagName, this.gql.name(node, exports.SPECIFIED_BY_TAG), [reason]); | ||
}; | ||
Extractor.prototype.property = function (node) { | ||
@@ -1255,3 +1237,3 @@ var tag = this.findTag(node, exports.FIELD_TAG); | ||
} | ||
var inner = this.collectPropertyType(node.type); | ||
var inner = this.collectType(node.type, { kind: "OUTPUT" }); | ||
// We already reported an error | ||
@@ -1274,3 +1256,2 @@ if (inner == null) | ||
argCount: null, | ||
asyncIterable: null, | ||
})); | ||
@@ -1285,6 +1266,6 @@ var killsParentOnExceptionDirective = this.killsParentOnExceptionDirective(node); | ||
// For input nodes and field may only be optional if `null` is a valid value. | ||
Extractor.prototype.collectType = function (node) { | ||
Extractor.prototype.collectType = function (node, ctx) { | ||
var _this = this; | ||
if (ts.isTypeReferenceNode(node)) { | ||
var type = this.typeReference(node); | ||
var type = this.typeReference(node, ctx); | ||
if (type == null) | ||
@@ -1295,3 +1276,3 @@ return null; | ||
else if (ts.isArrayTypeNode(node)) { | ||
var element = this.collectType(node.elementType); | ||
var element = this.collectType(node.elementType, ctx); | ||
if (element == null) | ||
@@ -1306,3 +1287,3 @@ return null; | ||
} | ||
var type = this.collectType(types[0]); | ||
var type = this.collectType(types[0], ctx); | ||
if (type == null) | ||
@@ -1325,3 +1306,3 @@ return null; | ||
else if (ts.isParenthesizedTypeNode(node)) { | ||
return this.collectType(node.type); | ||
return this.collectType(node.type, ctx); | ||
} | ||
@@ -1344,3 +1325,3 @@ else if (node.kind === ts.SyntaxKind.StringKeyword) { | ||
}; | ||
Extractor.prototype.typeReference = function (node) { | ||
Extractor.prototype.typeReference = function (node, ctx) { | ||
var identifier = this.expectNameIdentifier(node.typeName); | ||
@@ -1350,14 +1331,37 @@ if (identifier == null) | ||
var typeName = identifier.text; | ||
// Some types are not valid as input types. Validate that here: | ||
if (ctx.kind === "INPUT") { | ||
switch (typeName) { | ||
case "AsyncIterable": | ||
return this.report(node, "`AsyncIterable` is not a valid as an input type."); | ||
case "Promise": | ||
return this.report(node, "`Promise` is not a valid as an input type."); | ||
} | ||
} | ||
switch (typeName) { | ||
case "Array": | ||
case "Iterator": | ||
case "ReadonlyArray": { | ||
case "ReadonlyArray": | ||
case "AsyncIterable": { | ||
if (node.typeArguments == null) { | ||
return this.report(node, E.pluralTypeMissingParameter()); | ||
} | ||
var element = this.collectType(node.typeArguments[0]); | ||
var element = this.collectType(node.typeArguments[0], ctx); | ||
if (element == null) | ||
return null; | ||
return this.gql.nonNullType(node, this.gql.listType(node, element)); | ||
var listType = this.gql.listType(node, element); | ||
if (typeName === "AsyncIterable") { | ||
listType.isAsyncIterable = true; | ||
} | ||
return this.gql.nonNullType(node, listType); | ||
} | ||
case "Promise": { | ||
if (node.typeArguments == null) { | ||
return this.report(node, E.wrapperMissingTypeArg()); | ||
} | ||
var element = this.collectType(node.typeArguments[0], ctx); | ||
if (element == null) | ||
return null; | ||
return element; | ||
} | ||
default: { | ||
@@ -1407,2 +1411,8 @@ // We may not have encountered the definition of this type yet. So, we | ||
}; | ||
Extractor.prototype.hasTag = function (node, tagName) { | ||
var tags = ts | ||
.getJSDocTags(node) | ||
.filter(function (tag) { return tag.tagName.escapedText === tagName; }); | ||
return tags.length > 0; | ||
}; | ||
// It is a GraphQL best practice to model all fields as nullable. This allows | ||
@@ -1439,1 +1449,32 @@ // the server to handle field level executions by simply returning null for | ||
} | ||
// Given a node annotated as @gqlField, finds the parent node that is | ||
// expected to be annotated with @gqlType or @gqlInterface. | ||
// | ||
// Note that this is basically a reverse encoding of the traversal | ||
// we do from the @gqlType or @gqlInterface node to the fields. | ||
// This code needs to stay in sync with the traversal code, but should do so | ||
// safely since, if it doesn't match we'd end up with test errors. | ||
function getFieldParent(node) { | ||
if (ts.isMethodDeclaration(node) || | ||
ts.isGetAccessorDeclaration(node) || | ||
ts.isPropertyDeclaration(node)) { | ||
return node.parent; | ||
} | ||
else if (ts.isParameter(node)) { | ||
if (ts.isConstructorDeclaration(node.parent)) { | ||
return node.parent.parent; | ||
} | ||
return null; | ||
} | ||
else if (ts.isPropertySignature(node) || ts.isMethodSignature(node)) { | ||
if (ts.isTypeLiteralNode(node.parent) && | ||
ts.isTypeAliasDeclaration(node.parent.parent)) { | ||
return node.parent.parent; | ||
} | ||
else if (ts.isInterfaceDeclaration(node.parent)) { | ||
return node.parent; | ||
} | ||
return null; | ||
} | ||
return null; | ||
} |
@@ -15,3 +15,2 @@ import { ListTypeNode, NamedTypeNode, Location as GraphQLLocation, NameNode, Token, TypeNode, NonNullTypeNode, StringValueNode, ConstValueNode, ConstDirectiveNode, ConstArgumentNode, UnionTypeDefinitionNode, FieldDefinitionNode, InputValueDefinitionNode, FloatValueNode, IntValueNode, NullValueNode, BooleanValueNode, ConstListValueNode, ConstObjectValueNode, ConstObjectFieldNode, ObjectTypeDefinitionNode, EnumValueDefinitionNode, ScalarTypeDefinitionNode, InputObjectTypeDefinitionNode, EnumTypeDefinitionNode, InterfaceTypeDefinitionNode, DefinitionNode, Location, ASTNode } from "graphql"; | ||
argCount: number | null; | ||
asyncIterable?: ts.Node | null; | ||
}): ConstDirectiveNode; | ||
@@ -28,3 +27,3 @@ killsParentOnExceptionDirective(node: ts.Node): ConstDirectiveNode; | ||
enumValueDefinition(node: ts.Node, name: NameNode, directives: readonly ConstDirectiveNode[] | undefined, description: StringValueNode | null): EnumValueDefinitionNode; | ||
scalarTypeDefinition(node: ts.Node, name: NameNode, description: StringValueNode | null): ScalarTypeDefinitionNode; | ||
scalarTypeDefinition(node: ts.Node, name: NameNode, directives: readonly ConstDirectiveNode[] | null, description: StringValueNode | null): ScalarTypeDefinitionNode; | ||
inputObjectTypeDefinition(node: ts.Node, name: NameNode, fields: InputValueDefinitionNode[] | null, directives: readonly ConstDirectiveNode[] | null, description: StringValueNode | null): InputObjectTypeDefinitionNode; | ||
@@ -31,0 +30,0 @@ name(node: ts.Node, value: string): NameNode; |
@@ -31,5 +31,2 @@ "use strict"; | ||
} | ||
if (metadata.asyncIterable) { | ||
args.push(this.constArgument(metadata.asyncIterable, this.name(node, metadataDirectives_1.ASYNC_ITERABLE_ARG), this.boolean(node, true))); | ||
} | ||
return this.constDirective(node, this.name(node, metadataDirectives_1.FIELD_METADATA_DIRECTIVE), args); | ||
@@ -125,3 +122,3 @@ }; | ||
}; | ||
GraphQLConstructor.prototype.scalarTypeDefinition = function (node, name, description) { | ||
GraphQLConstructor.prototype.scalarTypeDefinition = function (node, name, directives, description) { | ||
return { | ||
@@ -132,3 +129,3 @@ kind: graphql_1.Kind.SCALAR_TYPE_DEFINITION, | ||
name: name, | ||
directives: undefined, | ||
directives: this._optionalList(directives), | ||
}; | ||
@@ -135,0 +132,0 @@ }; |
@@ -95,9 +95,9 @@ "use strict"; | ||
.map(function (doc) { return (0, filterNonGqlInterfaces_1.filterNonGqlInterfaces)(ctx, doc); }) | ||
// Resolve TypeScript type references to the GraphQL types they represent (or error). | ||
.andThen(function (doc) { return (0, resolveTypes_1.resolveTypes)(ctx, doc); }) | ||
// Ensure all subscription fields return an AsyncIterable. | ||
.andThen(function (doc) { return (0, validateAsyncIterable_1.validateAsyncIterable)(doc); }) | ||
// Apply default nullability to fields and arguments, and detect any misuse of | ||
// `@killsParentOnException`. | ||
.andThen(function (doc) { return (0, applyDefaultNullability_1.applyDefaultNullability)(doc, config); }) | ||
// Resolve TypeScript type references to the GraphQL types they represent (or error). | ||
.andThen(function (doc) { return (0, resolveTypes_1.resolveTypes)(ctx, doc); }) | ||
// Ensure all subscription fields, and _only_ subscription fields, return an AsyncIterable. | ||
.andThen(function (doc) { return (0, validateAsyncIterable_1.validateAsyncIterable)(doc); }) | ||
// Merge any `extend` definitions into their base definitions. | ||
@@ -104,0 +104,0 @@ .map(function (doc) { return (0, mergeExtensions_1.mergeExtensions)(doc); }) |
import { ConstDirectiveNode, DocumentNode, Location } from "graphql"; | ||
import { GratsDefinitionNode } from "./GraphQLConstructor"; | ||
/** | ||
* In most cases we can use directives to annotate constructs | ||
* however, it't not possible to annotate an individual TypeNode. | ||
* Additionally, we can't use sets or maps to "tag" nodes because | ||
* there are places where we immutably update the AST to make changes. | ||
* | ||
* Instead, we cheat and add properties to some nodes. These types use | ||
* interface merging to add our own properties to the AST. | ||
* | ||
* We try to use this approach sparingly. | ||
*/ | ||
declare module "graphql" { | ||
interface ListTypeNode { | ||
/** | ||
* Grats metadata: Whether the list type was defined as an AsyncIterable. | ||
* Used to ensure that all fields on `Subscription` return an AsyncIterable. | ||
*/ | ||
isAsyncIterable?: boolean; | ||
} | ||
} | ||
export declare const FIELD_METADATA_DIRECTIVE = "metadata"; | ||
@@ -16,5 +36,4 @@ export declare const FIELD_NAME_ARG = "name"; | ||
argCount: number | null; | ||
asyncIterable?: Location | null; | ||
}; | ||
export declare function makeKillsParentOnExceptionDirective(loc: Location): ConstDirectiveNode; | ||
export declare function parseFieldMetadataDirective(directive: ConstDirectiveNode): FieldMetadata; |
@@ -40,3 +40,3 @@ "use strict"; | ||
]); | ||
exports.DIRECTIVES_AST = (0, graphql_1.parse)("\n directive @".concat(exports.FIELD_METADATA_DIRECTIVE, "(\n \"\"\"\n Name of property/method/function. Defaults to field name. For\n function-backed fields, this is the function's export name.\n \"\"\"\n ").concat(exports.FIELD_NAME_ARG, ": String\n \"\"\"\n Path of the TypeScript module to import if the field is a function.\n \"\"\"\n ").concat(exports.TS_MODULE_PATH_ARG, ": String\n \"\"\"\n Number of arguments. No value means property access\n \"\"\"\n ").concat(exports.ARG_COUNT, ": Int\n \"\"\"\n Whether the field is an async iterable. If true, the argument's\n location is the typescript AsyncIterable identifier.\n \"\"\"\n ").concat(exports.ASYNC_ITERABLE_ARG, ": Boolean\n ) on FIELD_DEFINITION\n directive @").concat(exports.KILLS_PARENT_ON_EXCEPTION_DIRECTIVE, " on FIELD_DEFINITION\n")); | ||
exports.DIRECTIVES_AST = (0, graphql_1.parse)("\n directive @".concat(exports.FIELD_METADATA_DIRECTIVE, "(\n \"\"\"\n Name of property/method/function. Defaults to field name. For\n function-backed fields, this is the function's export name.\n \"\"\"\n ").concat(exports.FIELD_NAME_ARG, ": String\n \"\"\"\n Path of the TypeScript module to import if the field is a function.\n \"\"\"\n ").concat(exports.TS_MODULE_PATH_ARG, ": String\n \"\"\"\n Number of arguments. No value means property access\n \"\"\"\n ").concat(exports.ARG_COUNT, ": Int\n ) on FIELD_DEFINITION\n directive @").concat(exports.KILLS_PARENT_ON_EXCEPTION_DIRECTIVE, " on FIELD_DEFINITION\n")); | ||
function addMetadataDirectives(definitions) { | ||
@@ -56,12 +56,5 @@ return __spreadArray(__spreadArray([], __read(exports.DIRECTIVES_AST.definitions), false), __read(definitions), false); | ||
function parseFieldMetadataDirective(directive) { | ||
var _a; | ||
if (directive.name.value !== exports.FIELD_METADATA_DIRECTIVE) { | ||
throw new Error("Expected directive to be ".concat(exports.FIELD_METADATA_DIRECTIVE)); | ||
} | ||
var asyncIterableNode = (_a = directive.arguments) === null || _a === void 0 ? void 0 : _a.find(function (arg) { return arg.name.value === exports.ASYNC_ITERABLE_ARG; }); | ||
if ((asyncIterableNode === null || asyncIterableNode === void 0 ? void 0 : asyncIterableNode.value.kind) === graphql_1.Kind.BOOLEAN) { | ||
if (!asyncIterableNode.value.value) { | ||
throw new Error("Expected ".concat(exports.ASYNC_ITERABLE_ARG, " to be true")); | ||
} | ||
} | ||
return { | ||
@@ -71,3 +64,2 @@ name: getStringArg(directive, exports.FIELD_NAME_ARG), | ||
argCount: getIntArg(directive, exports.ARG_COUNT), | ||
asyncIterable: asyncIterableNode === null || asyncIterableNode === void 0 ? void 0 : asyncIterableNode.loc, | ||
}; | ||
@@ -74,0 +66,0 @@ } |
import { DocumentNode } from "graphql"; | ||
import { DiagnosticsResult } from "../utils/DiagnosticError"; | ||
/** | ||
* Ensure that all fields on `Subscription` return an AsyncIterable, and that no other | ||
* fields do. | ||
* Ensure that all fields on `Subscription` return an AsyncIterable and transform | ||
* the return type of subscription fields to not treat AsyncIterable as as list type. | ||
*/ | ||
export declare function validateAsyncIterable(doc: DocumentNode): DiagnosticsResult<DocumentNode>; |
"use strict"; | ||
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 }; | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); | ||
return __assign.apply(this, arguments); | ||
}; | ||
@@ -19,7 +19,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); | ||
var E = require("../Errors"); | ||
var metadataDirectives_1 = require("../metadataDirectives"); | ||
var helpers_1 = require("../utils/helpers"); | ||
/** | ||
* Ensure that all fields on `Subscription` return an AsyncIterable, and that no other | ||
* fields do. | ||
* Ensure that all fields on `Subscription` return an AsyncIterable and transform | ||
* the return type of subscription fields to not treat AsyncIterable as as list type. | ||
*/ | ||
@@ -29,9 +28,28 @@ function validateAsyncIterable(doc) { | ||
var errors = []; | ||
var visitNode = function (t) { | ||
var validateFieldsResult = validateField(t); | ||
if (validateFieldsResult != null) { | ||
errors.push(validateFieldsResult); | ||
var visitNode = { | ||
enter: function (t) { | ||
// Note: We assume the default name is used here. When custom operation types are supported | ||
// we'll need to update this. | ||
if (t.name.value !== "Subscription") { | ||
// Don't visit nodes that aren't the Subscription type. | ||
return false; | ||
} | ||
}, | ||
}; | ||
var visitSubscriptionField = function (field) { | ||
var inner = innerType(field.type); // Remove any non-null wrapper types | ||
if (inner.kind !== graphql_1.Kind.LIST_TYPE || !inner.isAsyncIterable) { | ||
errors.push((0, DiagnosticError_1.gqlErr)((0, helpers_1.loc)(field.type), E.subscriptionFieldNotAsyncIterable())); | ||
return field; | ||
} | ||
var itemType = inner.type; | ||
// If either field.type or item type is nullable, the field should be nullable | ||
if (isNullable(field.type) || isNullable(itemType)) { | ||
var innerInner = innerType(itemType); | ||
return __assign(__assign({}, field), { type: innerInner }); | ||
} | ||
// If _both_ are non-nullable, we will preserve the non-nullability. | ||
return __assign(__assign({}, field), { type: itemType }); | ||
}; | ||
(0, graphql_1.visit)(doc, (_a = {}, | ||
var newDoc = (0, graphql_1.visit)(doc, (_a = {}, | ||
_a[graphql_1.Kind.INTERFACE_TYPE_DEFINITION] = visitNode, | ||
@@ -41,2 +59,3 @@ _a[graphql_1.Kind.INTERFACE_TYPE_EXTENSION] = visitNode, | ||
_a[graphql_1.Kind.OBJECT_TYPE_EXTENSION] = visitNode, | ||
_a[graphql_1.Kind.FIELD_DEFINITION] = visitSubscriptionField, | ||
_a)); | ||
@@ -46,44 +65,13 @@ if (errors.length > 0) { | ||
} | ||
return (0, Result_1.ok)(doc); | ||
return (0, Result_1.ok)(newDoc); | ||
} | ||
exports.validateAsyncIterable = validateAsyncIterable; | ||
function validateField(t) { | ||
var e_1, _a; | ||
var _b; | ||
if (t.fields == null) | ||
return; | ||
// Note: We assume the default name is used here. When custom operation types are supported | ||
// we'll need to update this. | ||
var isSubscription = t.name.value === "Subscription" && | ||
(t.kind === graphql_1.Kind.OBJECT_TYPE_DEFINITION || | ||
t.kind === graphql_1.Kind.OBJECT_TYPE_EXTENSION); | ||
var isInterface = t.kind === graphql_1.Kind.INTERFACE_TYPE_DEFINITION || | ||
t.kind === graphql_1.Kind.INTERFACE_TYPE_EXTENSION; | ||
try { | ||
for (var _c = __values(t.fields), _d = _c.next(); !_d.done; _d = _c.next()) { | ||
var field = _d.value; | ||
var metadataDirective = (_b = field.directives) === null || _b === void 0 ? void 0 : _b.find(function (directive) { return directive.name.value === metadataDirectives_1.FIELD_METADATA_DIRECTIVE; }); | ||
if (isInterface && metadataDirective == null) { | ||
return; | ||
} | ||
if (metadataDirective == null) { | ||
throw new Error("Expected to find metadata directive on non-interface field \"".concat(t.name.value, ".").concat(field.name.value, "\".")); | ||
} | ||
var asyncIterable = (0, metadataDirectives_1.parseFieldMetadataDirective)(metadataDirective).asyncIterable; | ||
if (isSubscription && !asyncIterable) { | ||
return (0, DiagnosticError_1.gqlErr)((0, helpers_1.loc)(field.type), E.subscriptionFieldNotAsyncIterable()); | ||
} | ||
if (!isSubscription && asyncIterable) { | ||
return (0, DiagnosticError_1.gqlErr)(asyncIterable, // Arg location is the AsyncIterable type reference. | ||
E.nonSubscriptionFieldAsyncIterable()); | ||
} | ||
} | ||
function innerType(type) { | ||
if (type.kind === graphql_1.Kind.NON_NULL_TYPE) { | ||
return innerType(type.type); | ||
} | ||
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; } | ||
} | ||
return type; | ||
} | ||
function isNullable(t) { | ||
return t.kind !== graphql_1.Kind.NON_NULL_TYPE; | ||
} |
{ | ||
"name": "grats", | ||
"version": "0.0.23", | ||
"version": "0.0.24", | ||
"main": "dist/src/index.js", | ||
@@ -5,0 +5,0 @@ "bin": "dist/src/cli.js", |
@@ -60,1 +60,5 @@ # Grats: Implementation-First GraphQL for TypeScript | ||
- [typegraphql-reflection-poc](https://github.com/MichalLytek/typegraphql-reflection-poc) | ||
## License | ||
Grats is [MIT licensed](./LICENSE). |
298842
76
6140
64