diff --git a/.gitignore b/.gitignore index 0bda4814..c3dbd201 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ website/build examples/*/dist website/static/playground-dist yarn-error.log -coverage/* \ No newline at end of file +coverage diff --git a/docs/api-interfaceType.md b/docs/api-interfaceType.md index b7813831..b139942d 100644 --- a/docs/api-interfaceType.md +++ b/docs/api-interfaceType.md @@ -25,5 +25,3 @@ const User = objectType({ }, }); ``` - -If you need to modify the description or resolver defined by an interface, you can call the `modify` method on `objectType` to change these after the fact. diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 00000000..674f4d2c --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,7 @@ +# Nexus Plugins + +A "Plugin" is a way to extend the functionality of Nexus by tapping-into the +object/schema/type definitions for the GraphQL resolution lifecycle +in a way that is still type-safe. + +To get you started, we have a few "plugins" that Nexus ships with out-of the box. diff --git a/examples/apollo-fullstack/src/fullstack-typegen.ts b/examples/apollo-fullstack/src/fullstack-typegen.ts index 03bccb0a..9f1b9cfc 100644 --- a/examples/apollo-fullstack/src/fullstack-typegen.ts +++ b/examples/apollo-fullstack/src/fullstack-typegen.ts @@ -159,4 +159,14 @@ export interface NexusGenTypes { allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; abstractResolveReturn: NexusGenAbstractResolveReturnTypes; +} + + +declare global { + interface NexusGenPluginTypeConfig { + } + interface NexusGenPluginFieldConfig { + } + interface NexusGenPluginSchemaConfig { + } } \ No newline at end of file diff --git a/examples/ghost/src/generated/ghost-nexus.ts b/examples/ghost/src/generated/ghost-nexus.ts index fcaa53e5..b9ae16dc 100644 --- a/examples/ghost/src/generated/ghost-nexus.ts +++ b/examples/ghost/src/generated/ghost-nexus.ts @@ -156,3 +156,12 @@ export interface NexusGenTypes { abstractTypes: NexusGenTypes["interfaceNames"] | NexusGenTypes["unionNames"]; abstractResolveReturn: NexusGenAbstractResolveReturnTypes; } + +declare global { + interface NexusGenPluginTypeConfig {} + interface NexusGenPluginFieldConfig< + TypeName extends string, + FieldName extends string + > {} + interface NexusGenPluginSchemaConfig {} +} diff --git a/examples/githunt-api/src/githunt-typegen.ts b/examples/githunt-api/src/githunt-typegen.ts index 2293b1d9..fa0b95d5 100644 --- a/examples/githunt-api/src/githunt-typegen.ts +++ b/examples/githunt-api/src/githunt-typegen.ts @@ -184,4 +184,14 @@ export interface NexusGenTypes { allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; abstractResolveReturn: NexusGenAbstractResolveReturnTypes; +} + + +declare global { + interface NexusGenPluginTypeConfig { + } + interface NexusGenPluginFieldConfig { + } + interface NexusGenPluginSchemaConfig { + } } \ No newline at end of file diff --git a/examples/kitchen-sink/src/kitchen-sink-definitions.ts b/examples/kitchen-sink/src/kitchen-sink-definitions.ts index 941008b8..b8dd5aa0 100644 --- a/examples/kitchen-sink/src/kitchen-sink-definitions.ts +++ b/examples/kitchen-sink/src/kitchen-sink-definitions.ts @@ -182,7 +182,7 @@ export const Query = objectType({ resolve: () => "ok", }); t.list.date("dateAsList", () => []); - t.collection("collectionField", { + t.collectionField("collectionField", { type: Bar, args: { a: intArg(), diff --git a/examples/kitchen-sink/src/kitchen-sink-typegen.ts b/examples/kitchen-sink/src/kitchen-sink-typegen.ts index 69333ad6..2d1ae29d 100644 --- a/examples/kitchen-sink/src/kitchen-sink-typegen.ts +++ b/examples/kitchen-sink/src/kitchen-sink-typegen.ts @@ -13,7 +13,7 @@ declare global { } declare global { interface NexusGenCustomOutputMethods { - collection(fieldName: FieldName, opts: { + collectionField(fieldName: FieldName, opts: { type: NexusGenObjectNames | NexusGenInterfaceNames | core.NexusObjectTypeDef | core.NexusInterfaceTypeDef, nodes: core.SubFieldResolver, totalCount: core.SubFieldResolver, @@ -21,7 +21,8 @@ declare global { nullable?: boolean, description?: string }): void; - relayConnection(fieldName: FieldName, opts: { + relayConnectionField + (fieldName: FieldName, opts: { type: NexusGenObjectNames | NexusGenInterfaceNames | core.NexusObjectTypeDef | core.NexusInterfaceTypeDef, edges: core.SubFieldResolver, pageInfo: core.SubFieldResolver, @@ -29,6 +30,7 @@ declare global { nullable?: boolean, description?: string }): void + date(fieldName: FieldName, ...opts: core.ScalarOutSpread): void // "Date"; } } @@ -224,4 +226,14 @@ export interface NexusGenTypes { allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; abstractResolveReturn: NexusGenAbstractResolveReturnTypes; +} + + +declare global { + interface NexusGenPluginTypeConfig { + } + interface NexusGenPluginFieldConfig { + } + interface NexusGenPluginSchemaConfig { + } } \ No newline at end of file diff --git a/examples/star-wars/src/schema.ts b/examples/star-wars/src/schema.ts index 79c0a471..834c5f51 100644 --- a/examples/star-wars/src/schema.ts +++ b/examples/star-wars/src/schema.ts @@ -1,6 +1,6 @@ import * as path from "path"; import * as allTypes from "./graphql"; -import { makeSchema } from "nexus"; +import { makeSchema, plugin } from "nexus"; /** * Finally, we construct our schema (whose starting query type is the query @@ -21,4 +21,5 @@ export const schema = makeSchema({ ], contextType: "swapi.ContextType", }, + plugins: [plugin.AuthorizationPlugin], }); diff --git a/examples/star-wars/src/star-wars-typegen.ts b/examples/star-wars/src/star-wars-typegen.ts index a1efcf0f..051cc911 100644 --- a/examples/star-wars/src/star-wars-typegen.ts +++ b/examples/star-wars/src/star-wars-typegen.ts @@ -131,4 +131,37 @@ export interface NexusGenTypes { allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; abstractResolveReturn: NexusGenAbstractResolveReturnTypes; +} + + +import { core } from 'nexus'; +import { GraphQLResolveInfo } from 'graphql'; +export type AuthorizeResolver< + TypeName extends string, + FieldName extends string +> = ( + root: core.RootValue, + args: core.ArgsValue, + context: core.GetGen<"context">, + info: GraphQLResolveInfo +) => core.PromiseOrValue; + +declare global { + interface NexusGenPluginTypeConfig { + } + interface NexusGenPluginFieldConfig { + + /** + * Authorization for an individual field. Returning "true" + * or "Promise" means the field can be accessed. + * Returning "false" or "Promise" will respond + * with a "Not Authorized" error for the field. Returning + * or throwing an error will also prevent the resolver from + * executing. + */ + authorize?: AuthorizeResolver + + } + interface NexusGenPluginSchemaConfig { + } } \ No newline at end of file diff --git a/examples/ts-ast-reader/src/ts-ast-reader-typegen.ts b/examples/ts-ast-reader/src/ts-ast-reader-typegen.ts index 09b18692..da329805 100644 --- a/examples/ts-ast-reader/src/ts-ast-reader-typegen.ts +++ b/examples/ts-ast-reader/src/ts-ast-reader-typegen.ts @@ -17,8 +17,8 @@ export interface NexusGenInputs { } export interface NexusGenEnums { - NodeFlags: 4194304 | 16384 | 3 | 2 | 64 | 12679168 | 8192 | 2048 | 32 | 512 | 262144 | 1024 | 256 | 128 | 8388608 | 65536 | 2097152 | 16777216 | 1 | 16 | 4 | 0 | 1572864 | 524288 | 1048576 | 1408 | 384 | 8 | 32768 | 131072 | 20480 | 4194816 | 4096 - SyntaxKind: 118 | 54 | 69 | 49 | 120 | 185 | 187 | 169 | 197 | 212 | 119 | 63 | 41 | 62 | 40 | 121 | 58 | 201 | 122 | 55 | 70 | 50 | 146 | 9 | 204 | 186 | 218 | 123 | 73 | 229 | 285 | 191 | 160 | 71 | 51 | 246 | 271 | 74 | 274 | 75 | 240 | 209 | 76 | 19 | 23 | 21 | 57 | 314 | 27 | 149 | 205 | 175 | 7 | 77 | 157 | 124 | 166 | 161 | 78 | 228 | 317 | 79 | 236 | 125 | 152 | 272 | 80 | 198 | 81 | 82 | 223 | 25 | 24 | 190 | 83 | 220 | 316 | 1 | 243 | 84 | 278 | 35 | 33 | 37 | 59 | 36 | 34 | 52 | 254 | 255 | 85 | 257 | 221 | 211 | 86 | 259 | 87 | 88 | 59 | 28 | 60 | 118 | 109 | 288 | 299 | 73 | 8 | 148 | 18 | 73 | 14 | 0 | 2 | 163 | 226 | 89 | 227 | 225 | 144 | 239 | 196 | 90 | 165 | 158 | 126 | 145 | 32 | 67 | 68 | 48 | 47 | 30 | 273 | 72 | 91 | 222 | 109 | 250 | 249 | 248 | 92 | 253 | 183 | 180 | 162 | 127 | 176 | 93 | 287 | 94 | 241 | 110 | 174 | 128 | 289 | 300 | 302 | 301 | 296 | 303 | 294 | 292 | 291 | 293 | 304 | 310 | 305 | 298 | 299 | 308 | 306 | 309 | 288 | 297 | 307 | 290 | 295 | 267 | 268 | 263 | 266 | 260 | 270 | 264 | 262 | 265 | 261 | 269 | 11 | 12 | 129 | 233 | 71 | 71 | 71 | 147 | 117 | 310 | 310 | 147 | 14 | 71 | 108 | 17 | 147 | 7 | 183 | 31 | 66 | 46 | 29 | 28 | 111 | 182 | 181 | 315 | 214 | 156 | 155 | 61 | 45 | 39 | 258 | 245 | 244 | 130 | 3 | 256 | 252 | 247 | 251 | 131 | 132 | 192 | 95 | 4 | 213 | 14 | 312 | 96 | 135 | 8 | 184 | 136 | 188 | 147 | 210 | 18 | 22 | 20 | 171 | 112 | 151 | 195 | 177 | 313 | 65 | 43 | 60 | 44 | 38 | 203 | 202 | 113 | 189 | 275 | 154 | 153 | 114 | 115 | 148 | 56 | 133 | 13 | 134 | 172 | 97 | 230 | 217 | 26 | 159 | 137 | 6 | 276 | 2 | 64 | 42 | 284 | 277 | 208 | 116 | 138 | 10 | 98 | 99 | 232 | 139 | 311 | 215 | 193 | 206 | 15 | 16 | 216 | 17 | 100 | 178 | 101 | 234 | 53 | 102 | 103 | 235 | 170 | 242 | 194 | 140 | 168 | 199 | 104 | 179 | 150 | 163 | 167 | 164 | 141 | 173 | 142 | 0 | 143 | 282 | 280 | 279 | 286 | 283 | 281 | 237 | 238 | 219 | 105 | 200 | 106 | 107 | 224 | 5 | 108 | 231 | 207 | 117 + NodeFlags: ts.NodeFlags + SyntaxKind: ts.SyntaxKind } export interface NexusGenRootTypes { @@ -1593,4 +1593,14 @@ export interface NexusGenTypes { allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; abstractResolveReturn: NexusGenAbstractResolveReturnTypes; +} + + +declare global { + interface NexusGenPluginTypeConfig { + } + interface NexusGenPluginFieldConfig { + } + interface NexusGenPluginSchemaConfig { + } } \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 5e621389..01cdf738 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,4 +15,6 @@ module.exports = { moduleNameMapper: { "package.json": "/tests/stubs/package.json", }, + collectCoverageFrom: ["/src/**/*.ts"], + coverageReporters: ["lcov", "text", "html"], }; diff --git a/package.json b/package.json index e8ffc89f..fcac23b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nexus", - "version": "0.12.0-beta.6", + "version": "0.12.0-beta.7", "main": "dist", "types": "dist", "license": "MIT", @@ -8,8 +8,8 @@ "homepage": "https://nexus.js.org", "scripts": { "dev": "yarn link-examples && tsc -w", - "test": "jest", - "test:ci": "jest -i", + "test": "jest --watch", + "test:ci": "jest -i --coverage", "build": "tsc && prettier --write 'dist/**/*.ts'", "lint": "tslint -p tsconfig.json", "clean": "rm -rf dist", @@ -39,11 +39,12 @@ "tslib": "^1.9.3" }, "devDependencies": { - "@types/graphql": "14.0.7", + "@types/graphql": "^14.2.2", "@types/jest": "^23.3.7", "@types/node": "^10.12.2", "@types/prettier": "^1.15.2", - "graphql": "^14.0.2", + "@types/graphql-iso-date": "^3.3.1", + "graphql": "^14.2.0", "husky": "^1.1.2", "jest": "^23.6.0", "lint-staged": "^7.3.0", @@ -51,11 +52,13 @@ "ts-jest": "^23.10.4", "ts-node": "^7.0.1", "tslint": "^5.11.0", + "graphql-iso-date": "3.6.1", "tslint-config-prettier": "^1.15.0", - "typescript": "^3.4.5" + "typescript": "^3.4.5", + "source-map-support": "^0.5.12" }, "peerDependencies": { - "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0" + "graphql": "^14.2.0" }, "husky": { "hooks": { diff --git a/src/builder.ts b/src/builder.ts index feda9194..942c5417 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -36,6 +36,7 @@ import { assertValidName, getNamedType, GraphQLField, + GraphQLObjectTypeConfig, } from "graphql"; import { NexusArgConfig, @@ -61,7 +62,6 @@ import { NexusInterfaceTypeDef, } from "./definitions/interfaceType"; import { - FieldModificationDef, Implemented, NexusObjectTypeConfig, NexusObjectTypeDef, @@ -102,29 +102,38 @@ import { NonNullConfig, WrappedResolver, RootTypings, + Omit, MissingType, + GraphQLNamedInputType, } from "./definitions/_types"; import { TypegenAutoConfigOptions } from "./typegenAutoConfig"; -import { TypegenFormatFn } from "./typegenFormatPrettier"; -import { TypegenMetadata } from "./typegenMetadata"; -import { - AbstractTypeResolver, - GetGen, - AuthorizeResolver, -} from "./typegenTypeHelpers"; +import { TypegenMetadata, TypegenFormatFn } from "./typegenMetadata"; +import { AbstractTypeResolver, GetGen } from "./typegenTypeHelpers"; import { firstDefined, objValues, suggestionList, isObject, eachObj, + argsToConfig, isUnknownType, + Deferred, + deferred, } from "./utils"; import { NexusExtendInputTypeDef, NexusExtendInputTypeConfig, } from "./definitions/extendInputType"; import { DynamicInputMethodDef, DynamicOutputMethodDef } from "./dynamicMethod"; +import { + PluginDef, + wrapPluginsBefore, + PluginVisitBefore, + PluginVisitAfter, + wrapPluginsAfter, + PluginDefinitionInfo, +} from "./plugin"; +import { decorateType } from "./definitions/decorateType"; import { DynamicOutputPropertyDef } from "./dynamicProperty"; export type Maybe = T | null; @@ -147,18 +156,26 @@ const SCALARS: Record = { Boolean: GraphQLBoolean, }; -export const UNKNOWN_TYPE_SCALAR = new GraphQLScalarType({ - name: "NEXUS__UNKNOWN__TYPE__", - parseValue(value) { - return value; - }, - parseLiteral(value) { - return value; - }, - serialize(value) { - return value; - }, -}); +export const UNKNOWN_TYPE_SCALAR = decorateType( + new GraphQLScalarType({ + name: "NEXUS__UNKNOWN__TYPE", + description: `This scalar should never make it into production. It is used as a placeholder +for situations where GraphQL Nexus encounters a missing type. We don't want to +error immedately, otherwise the TypeScript definitions will not be updated.`, + parseValue(value) { + throw new Error("Error: NEXUS__UNKNOWN__TYPE is not a valid scalar."); + }, + parseLiteral(value) { + throw new Error("Error: NEXUS__UNKNOWN__TYPE is not a valid scalar."); + }, + serialize(value) { + throw new Error("Error: NEXUS__UNKNOWN__TYPE is not a valid scalar."); + }, + }), + { + rootTyping: "never", + } +); export interface BuilderConfig { /** @@ -213,6 +230,11 @@ export interface BuilderConfig { * Read more about how nexus handles nullability */ nonNullDefaults?: NonNullConfig; + /** + * List of plugins to apply to Nexus, with before/after hooks + * executed first to last: before -> resolve -> after + */ + plugins?: PluginDef[]; } export interface TypegenInfo { @@ -235,14 +257,26 @@ export interface TypegenInfo { contextType?: string; } -export interface SchemaConfig extends BuilderConfig { +export type SchemaConfig = BuilderConfig & { /** * All of the GraphQL types. This is an any for simplicity of developer experience, * if it's an object we get the values, if it's an array we flatten out the * valid types, ignoring invalid ones. */ types: any; -} + /** + * Optional callback, called when the type files have been written, or when + * the schema is created if the typegen should not be output. Primarily used internally + * for tests. + */ + onReady?: (err?: Error) => void; + /** + * When a referenced type is "missing" from the schema, we can look it up via a "missing method" + * type dispatch. This allows us to delegate the missing types to another schema, + * or construct them on-the-fly. + */ + findMissingType?: (typeName: string) => GraphQLNamedType | undefined; +} & NexusGenPluginSchemaConfig; export type TypeToWalk = | { type: "named"; value: GraphQLNamedType } @@ -335,6 +369,11 @@ export class SchemaBuilder { protected rootTypings: RootTypings = {}; /** + * All of the plugins for our server + */ + protected plugins: PluginDef[] = []; + + /* * Array of missing types */ protected missingTypes: Record = {}; @@ -344,18 +383,24 @@ export class SchemaBuilder { */ protected finalized: boolean = false; - constructor(protected config: BuilderConfig) { + constructor(protected config: SchemaConfig) { this.nonNullDefaults = { input: false, output: true, ...config.nonNullDefaults, }; + addTypes(this, config.types); + this.plugins = config.plugins || []; } getConfig(): BuilderConfig { return this.config; } + getPlugins(): PluginDef[] { + return this.plugins; + } + /** * Add type takes a Nexus type, or a GraphQL type and pulls * it into an internal "type registry". It also does an initial pass @@ -373,6 +418,7 @@ export class SchemaBuilder { | GraphQLNamedType | DynamicInputMethodDef | DynamicOutputMethodDef + | DynamicOutputPropertyDef ) { if (isNexusDynamicInputMethod(typeDef)) { this.dynamicInputFields[typeDef.name] = typeDef; @@ -475,7 +521,7 @@ export class SchemaBuilder { } } - getFinalTypeMap(): BuildTypes { + getFinalTypeMap>(): BuildTypes { this.finalized = true; this.walkTypes(); // If Query isn't defined, set it to null so it falls through to "missingType" @@ -513,7 +559,7 @@ export class SchemaBuilder { } }); return { - typeMap: this.finalTypeMap, + typeMap: this.finalTypeMap as any, dynamicFields: { dynamicInputFields: this.dynamicInputFields, dynamicOutputFields: this.dynamicOutputFields, @@ -554,18 +600,10 @@ export class SchemaBuilder { buildObjectType(config: NexusObjectTypeConfig) { const fields: NexusOutputFieldDef[] = []; const interfaces: Implemented[] = []; - const modifications: Record< - string, - FieldModificationDef[] - > = {}; const definitionBlock = new ObjectDefinitionBlock({ typeName: config.name, addField: (fieldDef) => fields.push(fieldDef), addInterfaces: (interfaceDefs) => interfaces.push(...interfaceDefs), - addFieldModifications(mods) { - modifications[mods.field] = modifications[mods.field] || []; - modifications[mods.field].push(mods); - }, addDynamicOutputMembers: (block, isList) => this.addDynamicOutputMembers(block, isList), }); @@ -580,48 +618,37 @@ export class SchemaBuilder { if (config.rootTyping) { this.rootTypings[config.name] = config.rootTyping; } + + const partialObjectConfig: Omit< + GraphQLObjectTypeConfig, + "fields" | "interfaces" + > = { + name: config.name, + description: config.description, + }; + return this.finalize( new GraphQLObjectType({ - name: config.name, + ...partialObjectConfig, interfaces: () => interfaces.map((i) => this.getInterface(i)), - description: config.description, fields: () => { const allFieldsMap: GraphQLFieldConfigMap = {}; const allInterfaces = interfaces.map((i) => this.getInterface(i)); allInterfaces.forEach((i) => { const interfaceFields = i.getFields(); - // We need to take the interface fields and reconstruct them - // this actually simplifies things becuase if we've modified - // the field at all it needs to happen here. + // Take the interface fields and reconstruct them with plugins, etc. Object.keys(interfaceFields).forEach((iFieldName) => { - const { isDeprecated, args, ...rest } = interfaceFields[ - iFieldName - ]; - allFieldsMap[iFieldName] = { - ...rest, - args: args.reduce( - (result: GraphQLFieldConfigArgumentMap, a) => { - const { name, ...argRest } = a; - result[name] = argRest; - return result; - }, - {} - ), - }; - const mods = modifications[iFieldName]; - if (mods) { - mods.map((mod) => { - if (typeof mod.description !== "undefined") { - allFieldsMap[iFieldName].description = mod.description; - } - if (typeof mod.resolve !== "undefined") { - allFieldsMap[iFieldName].resolve = mod.resolve; - } - }); - } + const interfaceField = interfaceFields[iFieldName]; + allFieldsMap[iFieldName] = this.buildInterfaceFieldForObject( + interfaceField, + config + ); }); }); - return this.buildObjectFields(fields, config, allFieldsMap); + fields.forEach((field) => { + allFieldsMap[field.name] = this.buildObjectField(field, config); + }); + return allFieldsMap; }, }) ); @@ -646,7 +673,7 @@ export class SchemaBuilder { }); } if (!resolveType) { - resolveType = this.missingResolveType(config.name, "union"); + resolveType = this.missingResolveType(config.name, "interface"); } if (config.rootTyping) { this.rootTypings[config.name] = config.rootTyping; @@ -654,7 +681,16 @@ export class SchemaBuilder { return this.finalize( new GraphQLInterfaceType({ name, - fields: () => this.buildObjectFields(fields, config, {}, true), + fields: () => { + const allInterfaceFields: GraphQLFieldConfigMap = {}; + fields.forEach((field) => { + allInterfaceFields[field.name] = this.buildInterfaceField( + field, + config + ); + }); + return allInterfaceFields; + }, resolveType, description, }) @@ -751,7 +787,11 @@ export class SchemaBuilder { fromObject: boolean = false ): GraphQLNamedType { invariantGuard(typeName); - if (typeName === "Query") { + let foundType: GraphQLNamedType | undefined; + if (this.config.findMissingType instanceof Function) { + foundType = this.config.findMissingType(typeName); + } + if (typeName === "Query" && !foundType) { return new GraphQLObjectType({ name: "Query", fields: { @@ -762,11 +802,9 @@ export class SchemaBuilder { }, }); } - if (!this.missingTypes[typeName]) { this.missingTypes[typeName] = { fromObject }; } - return UNKNOWN_TYPE_SCALAR; } @@ -782,6 +820,9 @@ export class SchemaBuilder { ); } members.forEach((member) => { + if (isNexusObjectTypeDef(member)) { + this.addType(member); + } unionMembers.push(this.getObjectType(member)); }); if (!unionMembers.length) { @@ -792,22 +833,6 @@ export class SchemaBuilder { return unionMembers; } - protected buildObjectFields( - fields: NexusOutputFieldDef[], - typeConfig: NexusObjectTypeConfig | NexusInterfaceTypeConfig, - intoObject: GraphQLFieldConfigMap, - forInterface: boolean = false - ): GraphQLFieldConfigMap { - fields.forEach((field) => { - intoObject[field.name] = this.buildObjectField( - field, - typeConfig, - forInterface - ); - }); - return intoObject; - } - protected buildInputObjectFields( fields: NexusInputFieldDef[], typeConfig: NexusInputObjectTypeConfig @@ -821,10 +846,41 @@ export class SchemaBuilder { protected buildObjectField( fieldConfig: NexusOutputFieldDef, - typeConfig: - | NexusObjectTypeConfig - | NexusInterfaceTypeConfig, - forInterface: boolean = false + typeConfig: NexusObjectTypeConfig + ): GraphQLFieldConfig { + if (!fieldConfig.type) { + throw new Error( + `Missing required "type" field for ${typeConfig.name}.${ + fieldConfig.name + }` + ); + } + const partialFieldConfig: Omit, "resolve"> = { + type: this.decorateType( + this.getOutputType(fieldConfig.type), + fieldConfig.list, + this.outputNonNull(typeConfig, fieldConfig) + ), + args: this.buildArgs(fieldConfig.args || {}, typeConfig), + description: fieldConfig.description, + deprecationReason: fieldConfig.deprecation, + subscribe: this.getSubscribe(fieldConfig), + }; + return { + ...partialFieldConfig, + resolve: this.getResolver({ + typeName: typeConfig.name, + fieldName: fieldConfig.name, + nexusTypeConfig: typeConfig, + graphqlFieldConfig: partialFieldConfig, + resolve: fieldConfig.resolve, + }), + }; + } + + protected buildInterfaceField( + fieldConfig: NexusOutputFieldDef, + typeConfig: NexusInterfaceTypeConfig ): GraphQLFieldConfig { if (!fieldConfig.type) { throw new Error( @@ -840,10 +896,33 @@ export class SchemaBuilder { this.outputNonNull(typeConfig, fieldConfig) ), args: this.buildArgs(fieldConfig.args || {}, typeConfig), - resolve: this.getResolver(fieldConfig, typeConfig, forInterface), + resolve: fieldConfig.resolve, description: fieldConfig.description, deprecationReason: fieldConfig.deprecation, - subscribe: forInterface ? undefined : this.getSubscribe(fieldConfig), + subscribe: undefined, + }; + } + + protected buildInterfaceFieldForObject( + fieldConfig: GraphQLField, + typeConfig: NexusObjectTypeConfig + ): GraphQLFieldConfig { + const { isDeprecated, deprecationReason, ...rest } = fieldConfig; + const partialFieldConfig: Omit, "resolve"> = { + ...rest, + args: argsToConfig(rest.args), + deprecationReason: isDeprecated ? deprecationReason : undefined, + }; + return { + ...partialFieldConfig, + resolve: this.getResolver({ + fieldName: fieldConfig.name, + typeName: typeConfig.name, + // nexusFieldConfig: fieldConfig, - todo, get this from an interface lookup + nexusTypeConfig: typeConfig, + graphqlFieldConfig: partialFieldConfig, + resolve: fieldConfig.resolve, + }), }; } @@ -1003,9 +1082,13 @@ export class SchemaBuilder { protected getInputType( name: | string + | GraphQLNamedInputType | AllNexusInputTypeDefs | NexusWrappedType ): GraphQLPossibleInputs { + if (isNamedType(name)) { + return name; + } const type = this.getOrBuildType(name); if (!isInputObjectType(type) && !isLeafType(type)) { throw new Error( @@ -1094,30 +1177,58 @@ export class SchemaBuilder { let subscribe: undefined | GraphQLFieldResolver; if (fieldConfig.subscribe) { subscribe = fieldConfig.subscribe; - if (fieldConfig.authorize) { - subscribe = wrapAuthorize(subscribe, fieldConfig.authorize); - } } return subscribe; } - protected getResolver( - fieldConfig: NexusOutputFieldDef, - typeConfig: NexusObjectTypeConfig | NexusInterfaceTypeConfig, - forInterface: boolean = false - ) { + protected getResolver(getResolverConfig: GetResolverConfig) { let resolver: undefined | GraphQLFieldResolver; - if (fieldConfig.resolve) { - resolver = fieldConfig.resolve; - } - if (!resolver && !forInterface) { - resolver = (typeConfig as NexusObjectTypeConfig).defaultResolver; + if (getResolverConfig.resolve) { + resolver = getResolverConfig.resolve; + } + if (!resolver && getResolverConfig.nexusTypeConfig) { + resolver = (getResolverConfig.nexusTypeConfig as NexusObjectTypeConfig< + any + >).defaultResolver; + } + let finalResolver = resolver; + if (this.plugins.length) { + const before: [string, PluginVisitBefore][] = []; + const after: [string, PluginVisitAfter][] = []; + // Execute all of the plugins for each individual resolver. + for (let i = 0; i < this.plugins.length; i++) { + const addedPlugin = this.plugins[i].config; + if (addedPlugin.pluginDefinition) { + const returnDef = addedPlugin.pluginDefinition({ + ...getResolverConfig, + nexusSchemaConfig: this.config, + mutableObj: {}, + }); + if (returnDef) { + if (returnDef.before) { + before.push([addedPlugin.name, returnDef.before]); + } + if (returnDef.after) { + after.push([addedPlugin.name, returnDef.after]); + } + } + } + } + if (before.length) { + finalResolver = wrapPluginsBefore( + resolver || defaultFieldResolver, + before + ); + } + if (after.length) { + finalResolver = wrapPluginsAfter( + resolver || defaultFieldResolver, + after + ); + } } - if (fieldConfig.authorize && typeConfig.name !== "Subscription") { - resolver = wrapAuthorize( - resolver || defaultFieldResolver, - fieldConfig.authorize - ); + if (resolver && finalResolver) { + (resolver as WrappedResolver).nexusWrappedResolver = finalResolver; } return resolver; } @@ -1218,7 +1329,6 @@ export class SchemaBuilder { protected walkOutputType(obj: T) { const definitionBlock = new ObjectDefinitionBlock({ typeName: obj.name, - addFieldModifications: () => {}, addInterfaces: () => {}, addField: (f) => this.maybeTraverseOutputType(f), addDynamicOutputMembers: (block, isList) => @@ -1290,33 +1400,6 @@ function extendError(name: string) { ); } -export function wrapAuthorize( - resolver: GraphQLFieldResolver, - authorize: AuthorizeResolver -): GraphQLFieldResolver { - const nexusAuthWrapped: WrappedResolver = async (root, args, ctx, info) => { - const authResult = await authorize(root, args, ctx, info); - if (authResult === true) { - return resolver(root, args, ctx, info); - } - if (authResult === false) { - throw new Error("Not authorized"); - } - if (authResult instanceof Error) { - throw authResult; - } - const { - fieldName, - parentType: { name: parentTypeName }, - } = info; - throw new Error( - `Nexus authorize for ${parentTypeName}.${fieldName} Expected a boolean or Error, saw ${authResult}` - ); - }; - nexusAuthWrapped.nexusWrappedResolver = resolver; - return nexusAuthWrapped; -} - export type DynamicFieldDefs = { dynamicInputFields: DynamicInputFields; dynamicOutputFields: DynamicOutputFields; @@ -1324,7 +1407,7 @@ export type DynamicFieldDefs = { }; export interface BuildTypes< - TypeMapDefs extends Record + TypeMapDefs extends Record = any > { typeMap: TypeMapDefs; dynamicFields: DynamicFieldDefs; @@ -1337,18 +1420,6 @@ export interface BuildTypes< * better developer experience. This is primarily useful for testing * type generation */ -export function buildTypes< - TypeMapDefs extends Record = any ->( - types: any, - config: BuilderConfig = { outputs: false }, - schemaBuilder?: SchemaBuilder -): BuildTypes { - const builder = schemaBuilder || new SchemaBuilder(config); - addTypes(builder, types); - return builder.getFinalTypeMap(); -} - function addTypes(builder: SchemaBuilder, types: any) { if (!types) { return; @@ -1363,7 +1434,8 @@ function addTypes(builder: SchemaBuilder, types: any) { isNexusExtendInputTypeDef(types) || isNamedType(types) || isNexusDynamicInputMethod(types) || - isNexusDynamicOutputMethod(types) + isNexusDynamicOutputMethod(types) || + isNexusDynamicOutputProperty(types) ) { builder.addType(types); } else if (Array.isArray(types)) { @@ -1382,19 +1454,26 @@ export type NexusSchema = GraphQLSchema & { extensions: Record & { nexus: NexusSchemaExtensions }; }; +export type MakeSchemaInternalValue = { + schema: NexusSchema; + builder: SchemaBuilder; + missingTypes: Record; +}; + /** * Builds the schema, we may return more than just the schema * from this one day. */ export function makeSchemaInternal( - options: SchemaConfig, - schemaBuilder?: SchemaBuilder -): { schema: NexusSchema; missingTypes: Record } { - const { typeMap, dynamicFields, rootTypings, missingTypes } = buildTypes( - options.types, - options, - schemaBuilder - ); + config: SchemaConfig +): MakeSchemaInternalValue { + const builder = new SchemaBuilder(config); + const { + typeMap, + dynamicFields, + rootTypings, + missingTypes, + } = builder.getFinalTypeMap(); let { Query, Mutation, Subscription } = typeMap; if (!isObjectType(Query)) { @@ -1430,7 +1509,7 @@ export function makeSchemaInternal( dynamicFields, }, }; - return { schema, missingTypes }; + return { schema, builder, missingTypes }; } /** @@ -1440,8 +1519,8 @@ export function makeSchemaInternal( * Requires at least one type be named "Query", which will be used as the * root query type. */ -export function makeSchema(options: SchemaConfig): GraphQLSchema { - const { schema, missingTypes } = makeSchemaInternal(options); +export function makeSchema(config: SchemaConfig): NexusSchema { + const { schema, builder, missingTypes } = makeSchemaInternal(config); // Only in development envs do we want to worry about regenerating the // schema definition and/or generated types. @@ -1449,17 +1528,21 @@ export function makeSchema(options: SchemaConfig): GraphQLSchema { shouldGenerateArtifacts = Boolean( !process.env.NODE_ENV || process.env.NODE_ENV === "development" ), - } = options; + } = config; + + let dfd: Deferred | undefined; if (shouldGenerateArtifacts) { + dfd = deferred(); // Generating in the next tick allows us to use the schema // in the optional thunk for the typegen config - new TypegenMetadata(options).generateArtifacts(schema).catch((e) => { - console.error(e); - }); + new TypegenMetadata(builder, schema) + .generateArtifacts() + .then(dfd.resolve) + .catch(dfd.reject); } - assertNoMissingTypes(schema, missingTypes); + assertNoMissingTypes(schema, missingTypes, dfd, config.onReady); return schema; } @@ -1485,37 +1568,67 @@ function normalizeArg( return arg({ type: argVal }); } -function assertNoMissingTypes( +export type GetResolverConfig = Omit< + PluginDefinitionInfo, + "nexusSchemaConfig" | "mutableObj" +> & { resolve: GraphQLFieldResolver | undefined }; + +/** + * If there are any missing types, we want to throw those after the schema is output, so + * we are able to generate the types properly. + * + * @param schema + * @param missingTypes + */ +export function assertNoMissingTypes( schema: GraphQLSchema, - missingTypes: Record + missingTypes: Record, + dfd?: Deferred, + onReady?: (err?: Error) => void ) { const missingTypesNames = Object.keys(missingTypes); const schemaTypeMap = schema.getTypeMap(); const schemaTypeNames = Object.keys(schemaTypeMap).filter( (typeName) => !isUnknownType(schemaTypeMap[typeName]) ); - if (missingTypesNames.length > 0) { const errors = missingTypesNames .map((typeName) => { const { fromObject } = missingTypes[typeName]; - if (fromObject) { return `- Looks like you forgot to import ${typeName} in the root "types" passed to Nexus makeSchema`; } - const suggestions = suggestionList(typeName, schemaTypeNames); - let suggestionsString = ""; - if (suggestions.length > 0) { suggestionsString = ` or mean ${suggestions.join(", ")}`; } - return `- Missing type ${typeName}, did you forget to import a type to the root query${suggestionsString}?`; }) .join("\n"); - - throw new Error("\n" + errors); + const err = new Error("\n" + errors); + if (dfd) { + dfd.promise + .then(() => Promise.reject(err)) + .catch((e) => { + if (onReady) { + onReady(e); + } + process.nextTick(() => { + throw err; + }); + }); + } else { + if (onReady) { + onReady(err); + } + throw err; + } + } else if (onReady) { + if (dfd) { + dfd.promise.then(() => onReady(), onReady); + } else { + onReady(); + } } } diff --git a/src/core.ts b/src/core.ts index d695e768..db875fbf 100644 --- a/src/core.ts +++ b/src/core.ts @@ -4,7 +4,6 @@ export * from "./builder"; export * from "./sdlConverter"; export * from "./typegen"; export * from "./typegenAutoConfig"; -export * from "./typegenFormatPrettier"; export * from "./typegenMetadata"; export * from "./typegenTypeHelpers"; export * from "./utils"; diff --git a/src/definitions/_types.ts b/src/definitions/_types.ts index 4fb3f200..7d4948e9 100644 --- a/src/definitions/_types.ts +++ b/src/definitions/_types.ts @@ -3,6 +3,11 @@ import { GraphQLCompositeType, GraphQLInputObjectType, GraphQLFieldResolver, + GraphQLScalarType, + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLUnionType, + GraphQLEnumType, } from "graphql"; export type WrappedResolver = GraphQLFieldResolver & { @@ -26,9 +31,10 @@ export enum NexusTypes { WrappedType = "WrappedType", OutputField = "OutputField", InputField = "InputField", - DynamicInput = "DynamicInput", + DynamicInputMethod = "DynamicInputMethod", DynamicOutputMethod = "DynamicOutputMethod", DynamicOutputProperty = "DynamicOutputProperty", + Plugin = "Plugin", } export interface DeprecationInfo { @@ -111,3 +117,15 @@ export interface RootTypingImport { export interface MissingType { fromObject: boolean; } + +export type GraphQLNamedOutputType = + | GraphQLScalarType + | GraphQLObjectType + | GraphQLInterfaceType + | GraphQLUnionType + | GraphQLEnumType; + +export type GraphQLNamedInputType = + | GraphQLScalarType + | GraphQLInputObjectType + | GraphQLEnumType; diff --git a/src/definitions/decorateType.ts b/src/definitions/decorateType.ts index a6526b9c..2d61ef45 100644 --- a/src/definitions/decorateType.ts +++ b/src/definitions/decorateType.ts @@ -2,7 +2,7 @@ import { GraphQLNamedType } from "graphql"; import { RootTypingDef } from "./_types"; export interface TypeExtensionConfig { - asNexusMethod: string; + asNexusMethod?: string; rootTyping?: RootTypingDef; } diff --git a/src/definitions/definitionBlocks.ts b/src/definitions/definitionBlocks.ts index acbc75c9..4242b3e5 100644 --- a/src/definitions/definitionBlocks.ts +++ b/src/definitions/definitionBlocks.ts @@ -4,16 +4,15 @@ import { GetGen, HasGen3, NeedsResolver, - AuthorizeResolver, GetGen3, } from "../typegenTypeHelpers"; import { ArgsRecord } from "./args"; import { - AllNexusOutputTypeDefs, NexusWrappedType, + AllNexusOutputTypeDefs, AllNexusInputTypeDefs, } from "./wrapping"; -import { BaseScalars } from "./_types"; +import { BaseScalars, GraphQLNamedInputType } from "./_types"; import { GraphQLFieldResolver } from "graphql"; export interface CommonFieldConfig { @@ -43,24 +42,15 @@ export interface CommonFieldConfig { list?: true | boolean[]; } -export interface CommonOutputFieldConfig< +export type CommonOutputFieldConfig< TypeName extends string, FieldName extends string -> extends CommonFieldConfig { +> = CommonFieldConfig & { /** * Arguments for the field */ args?: ArgsRecord; - /** - * Authorization for an individual field. Returning "true" - * or "Promise" means the field can be accessed. - * Returning "false" or "Promise" will respond - * with a "Not Authorized" error for the field. Returning - * or throwing an error will also prevent the resolver from - * executing. - */ - authorize?: AuthorizeResolver; -} +} & NexusGenPluginFieldConfig; export interface OutputScalarConfig< TypeName extends string, @@ -262,7 +252,10 @@ export interface NexusInputFieldConfig< TypeName extends string, FieldName extends string > extends ScalarInputFieldConfig> { - type: GetGen<"allInputTypes", string> | AllNexusInputTypeDefs; + type: + | GetGen<"allInputTypes", string> + | AllNexusInputTypeDefs + | GraphQLNamedInputType; } export type NexusInputFieldDef = NexusInputFieldConfig & { diff --git a/src/definitions/objectType.ts b/src/definitions/objectType.ts index 3d5205cb..0762a893 100644 --- a/src/definitions/objectType.ts +++ b/src/definitions/objectType.ts @@ -31,19 +31,9 @@ export interface FieldModification< resolve?: FieldResolver; } -export interface FieldModificationDef< - TypeName extends string, - FieldName extends string -> extends FieldModification { - field: FieldName; -} - export interface ObjectDefinitionBuilder extends OutputDefinitionBuilder { addInterfaces(toAdd: Implemented[]): void; - addFieldModifications( - changes: FieldModificationDef - ): void; } export class ObjectDefinitionBlock< @@ -58,17 +48,17 @@ export class ObjectDefinitionBlock< implements(...interfaceName: Array) { this.typeBuilder.addInterfaces(interfaceName); } - /** - * Modifies a field added via an interface - */ + modify< FieldName extends Extract, string> >(field: FieldName, modifications: FieldModification) { - this.typeBuilder.addFieldModifications({ ...modifications, field }); + throw new Error( + "This method has been removed, if you were using it - please open an issue so we can discuss a suitable API replacement" + ); } } -export interface NexusObjectTypeConfig { +export type NexusObjectTypeConfig = { name: TypeName; definition(t: ObjectDefinitionBlock): void; /** @@ -90,7 +80,7 @@ export interface NexusObjectTypeConfig { * Root type information for this type */ rootTyping?: RootTypingDef; -} +} & NexusGenPluginTypeConfig; export class NexusObjectTypeDef { constructor( diff --git a/src/definitions/subscriptionField.ts b/src/definitions/subscriptionField.ts index 5cd37427..7458e6da 100644 --- a/src/definitions/subscriptionField.ts +++ b/src/definitions/subscriptionField.ts @@ -3,10 +3,10 @@ import { CommonOutputFieldConfig } from "./definitionBlocks"; import { GraphQLResolveInfo } from "graphql"; import { ResultValue, - MaybePromiseDeep, + PromiseOrValueDeep, ArgsValue, GetGen, - MaybePromise, + PromiseOrValue, } from "../typegenTypeHelpers"; import { AllNexusOutputTypeDefs, NexusWrappedType } from "./wrapping"; import { AsyncIterator } from "./_types"; @@ -26,7 +26,7 @@ export interface SubscribeFieldConfig< args: ArgsValue, ctx: GetGen<"context">, info: GraphQLResolveInfo - ): MaybePromise> | MaybePromiseDeep>; + ): PromiseOrValue> | PromiseOrValueDeep>; /** * Resolve method for the field @@ -37,8 +37,8 @@ export interface SubscribeFieldConfig< context: GetGen<"context">, info: GraphQLResolveInfo ): - | MaybePromise> - | MaybePromiseDeep>; + | PromiseOrValue> + | PromiseOrValueDeep>; } export function subscriptionField( diff --git a/src/definitions/wrapping.ts b/src/definitions/wrapping.ts index d73a4181..347d6dcc 100644 --- a/src/definitions/wrapping.ts +++ b/src/definitions/wrapping.ts @@ -147,7 +147,7 @@ export function isNexusDynamicOutputProperty( ): obj is DynamicOutputPropertyDef { return ( isNexusTypeDef(obj) && - obj[NexusWrappedSymbol] === NexusTypes.DynamicOutputMethod + obj[NexusWrappedSymbol] === NexusTypes.DynamicOutputProperty ); } export function isNexusDynamicOutputMethod( @@ -162,6 +162,7 @@ export function isNexusDynamicInputMethod( obj: any ): obj is DynamicInputMethodDef { return ( - isNexusTypeDef(obj) && obj[NexusWrappedSymbol] === NexusTypes.DynamicInput + isNexusTypeDef(obj) && + obj[NexusWrappedSymbol] === NexusTypes.DynamicInputMethod ); } diff --git a/src/dynamicMethod.ts b/src/dynamicMethod.ts index 4d71412b..49654feb 100644 --- a/src/dynamicMethod.ts +++ b/src/dynamicMethod.ts @@ -63,7 +63,7 @@ export class DynamicInputMethodDef { return this.config; } } -withNexusSymbol(DynamicInputMethodDef, NexusTypes.DynamicInput); +withNexusSymbol(DynamicInputMethodDef, NexusTypes.DynamicInputMethod); export class DynamicOutputMethodDef { constructor( @@ -81,7 +81,7 @@ withNexusSymbol(DynamicOutputMethodDef, NexusTypes.DynamicOutputMethod); * for an output type, taking arbitrary input to define * additional types. * - * t.collection('posts', { + * t.collectionField('posts', { * nullable: true, * totalCount(root, args, ctx, info) { * return ctx.user.getTotalPostCount(root.id, args) diff --git a/src/dynamicProperty.ts b/src/dynamicProperty.ts index 07086917..828c05c7 100644 --- a/src/dynamicProperty.ts +++ b/src/dynamicProperty.ts @@ -29,7 +29,7 @@ export class DynamicOutputPropertyDef { return this.config; } } -withNexusSymbol(DynamicOutputPropertyDef, NexusTypes.DynamicOutputMethod); +withNexusSymbol(DynamicOutputPropertyDef, NexusTypes.DynamicOutputProperty); /** * Defines a new property on the object definition block diff --git a/src/extensions/collection.ts b/src/extensions/collection.ts index 6424f7e3..1a42271f 100644 --- a/src/extensions/collection.ts +++ b/src/extensions/collection.ts @@ -5,8 +5,8 @@ import { intArg } from "../definitions/args"; const basicCollectionMap = new Map>(); -export const Collection = dynamicOutputMethod({ - name: "collection", +export const CollectionMethod = dynamicOutputMethod({ + name: "collectionField", typeDefinition: `(fieldName: FieldName, opts: { type: NexusGenObjectNames | NexusGenInterfaceNames | core.NexusObjectTypeDef | core.NexusInterfaceTypeDef, nodes: core.SubFieldResolver, @@ -16,27 +16,32 @@ export const Collection = dynamicOutputMethod({ description?: string }): void;`, factory({ typeDef: t, args: [fieldName, config] }) { - const type = + if (!config.type) { + throw new Error( + `Missing required property "type" from collectionField ${fieldName}` + ); + } + const typeName = typeof config.type === "string" ? config.type : config.type.name; if (config.list) { throw new Error( - `Collection field ${fieldName}.${type} cannot be used as a list.` + `Collection field ${fieldName}.${typeName} cannot be used as a list.` ); } - if (!basicCollectionMap.has(type)) { + if (!basicCollectionMap.has(typeName)) { basicCollectionMap.set( - type, + typeName, objectType({ - name: `${type}Collection`, + name: `${typeName}Collection`, definition(c) { c.int("totalCount"); - c.list.field("nodes", { type }); + c.list.field("nodes", { type: config.type }); }, }) ); } t.field(fieldName, { - type: basicCollectionMap.get(type)!, + type: basicCollectionMap.get(typeName)!, args: config.args || { page: intArg(), perPage: intArg(), diff --git a/src/extensions/relayConnection.ts b/src/extensions/relayConnection.ts index 3ac5ae3d..74012063 100644 --- a/src/extensions/relayConnection.ts +++ b/src/extensions/relayConnection.ts @@ -7,18 +7,25 @@ const relayConnectionMap = new Map>(); let pageInfo: NexusObjectTypeDef; -export const RelayConnection = dynamicOutputMethod({ - name: "relayConnection", - typeDefinition: `(fieldName: FieldName, opts: { +export const RelayConnectionMethod = dynamicOutputMethod({ + name: "relayConnectionField", + typeDefinition: ` + (fieldName: FieldName, opts: { type: NexusGenObjectNames | NexusGenInterfaceNames | core.NexusObjectTypeDef | core.NexusInterfaceTypeDef, edges: core.SubFieldResolver, pageInfo: core.SubFieldResolver, args?: Record>, nullable?: boolean, description?: string - }): void`, + }): void + `, factory({ typeDef: t, args: [fieldName, config] }) { - const type = + if (!config.type) { + throw new Error( + `Missing required property "type" from relayConnection field ${fieldName}` + ); + } + const typeName = typeof config.type === "string" ? config.type : config.type.name; pageInfo = pageInfo || @@ -31,18 +38,18 @@ export const RelayConnection = dynamicOutputMethod({ }); if (config.list) { throw new Error( - `Collection field ${fieldName}.${type} cannot be used as a list.` + `Collection field ${fieldName}.${typeName} cannot be used as a list.` ); } - if (!relayConnectionMap.has(config.type)) { + if (!relayConnectionMap.has(typeName)) { relayConnectionMap.set( - config.type, + typeName, objectType({ - name: `${config.type}RelayConnection`, + name: `${typeName}RelayConnection`, definition(c) { c.list.field("edges", { type: objectType({ - name: `${config.type}Edge`, + name: `${typeName}Edge`, definition(e) { e.id("cursor"); e.field("node", { type: config.type }); @@ -55,7 +62,7 @@ export const RelayConnection = dynamicOutputMethod({ ); } t.field(fieldName, { - type: relayConnectionMap.get(config.type)!, + type: relayConnectionMap.get(typeName)!, args: { first: intArg(), after: stringArg(), diff --git a/src/fileSystem.ts b/src/fileSystem.ts new file mode 100644 index 00000000..3a3e006a --- /dev/null +++ b/src/fileSystem.ts @@ -0,0 +1,66 @@ +import path from "path"; + +export class FileSystem { + protected static cachedInstance: FileSystem; + + static getInstance() { + if (!this.cachedInstance) { + this.cachedInstance = new FileSystem(); + } + return this.cachedInstance; + } + + protected get util() { + return require("util") as typeof import("util"); + } + + protected get fs() { + return require("fs") as typeof import("fs"); + } + + protected get mkdir() { + return this.util.promisify(this.fs.mkdir); + } + + protected get readFile() { + return this.util.promisify(this.fs.readFile); + } + + protected get writeFile() { + return this.util.promisify(this.fs.writeFile); + } + + protected get unlink() { + return this.util.promisify(this.fs.unlink); + } + + protected ensureAbsolute(filePath: string) { + if (!path.isAbsolute(filePath)) { + throw new Error( + `GraphQL Nexus: Expected an absolute path, saw ${filePath}` + ); + } + return filePath; + } + + getFile(fileName: string) { + return this.readFile(this.ensureAbsolute(fileName), "utf8"); + } + + /** + * Unlinks & writes a new file - useful when we are generating the + * type definitions, as this causes the ts server to know the file has changed. + * @param path + * @param toSave + */ + async replaceFile(filePath: string, toSave: string) { + if (this.fs.existsSync(this.ensureAbsolute(filePath))) { + await this.unlink(filePath); + } + const dirName = path.dirname(filePath); + if (!this.fs.existsSync(dirName)) { + await this.mkdir(dirName, { recursive: true }); + } + await this.writeFile(filePath, toSave); + } +} diff --git a/src/index.ts b/src/index.ts index 934e68c0..f4a29e8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ // All of the Public API definitions -export { buildTypes, makeSchema } from "./builder"; +export { makeSchema } from "./builder"; export { arg, booleanArg, @@ -30,7 +30,8 @@ export { FieldType, } from "./typegenTypeHelpers"; export { dynamicInputMethod, dynamicOutputMethod } from "./dynamicMethod"; -export { core, blocks, ext }; +export { core, blocks, ext, plugin }; import * as core from "./core"; import * as blocks from "./blocks"; import * as ext from "./extensions"; +import * as plugin from "./plugins/_all"; diff --git a/src/plugin.ts b/src/plugin.ts new file mode 100644 index 00000000..57752c01 --- /dev/null +++ b/src/plugin.ts @@ -0,0 +1,233 @@ +import { SchemaConfig } from "./builder"; +import { + GraphQLFieldResolver, + GraphQLResolveInfo, + GraphQLFieldConfig, +} from "graphql"; +import { withNexusSymbol, NexusTypes, Omit } from "./definitions/_types"; +import { isPromise } from "./utils"; +import { NexusObjectTypeConfig } from "./definitions/objectType"; +import { NexusOutputFieldConfig } from "./definitions/definitionBlocks"; + +export class PluginDef { + constructor(readonly config: PluginConfig) {} +} +withNexusSymbol(PluginDef, NexusTypes.Plugin); + +export type PluginVisitAfter = ( + result: any, + root: any, + args: any, + ctx: any, + info: GraphQLResolveInfo, + breakVal: typeof BREAK_RESULT_VAL +) => any; + +export type PluginVisitBefore = ( + root: any, + args: any, + ctx: any, + info: GraphQLResolveInfo, + nextVal: typeof NEXT_RESULT_VAL +) => any; + +export type PluginVisitor = { + after?: PluginVisitAfter; + before?: PluginVisitBefore; +}; + +export type PluginDefinitionInfo = { + /** + * The name of the type we're applying the plugin to + */ + typeName: string; + /** + * The name of the field we're applying the plugin to + */ + fieldName: string; + /** + * The root-level SchemaConfig passed + */ + nexusSchemaConfig: Omit & Record; + /** + * The config provided to the Nexus type containing the field. + * Will not exist if this is a non-Nexus GraphQL type. + */ + nexusTypeConfig?: Omit, "definition"> & + Record; + /** + * The config provided to the Nexus type containing the field. + * Will not exist if this is a non-Nexus GraphQL type. + */ + nexusFieldConfig?: Omit, "resolve"> & + Record; + /** + * Info about the GraphQL Field we're decorating. + * Always guaranteed to exist, even for non-Nexus GraphQL types + */ + graphqlFieldConfig: Omit, "resolve">; + /** + * If we need to collect/reference metadata during the + * plugin middleware definition stage, you can use this object. + * + * An example use would be to collect all fields that have a "validation" + * property for their input to reference in runtime. After the schema is complete, + * this object is frozen so it is not abused at runtime. + */ + mutableObj: Record; +}; + +export interface RootTypingImport { + /** + * File path to import the type from. + */ + path: string; + /** + * Name of the type we want to reference in the `path` + */ + name: string; + /** + * Name we want the imported type to be referenced as + */ + alias?: string; +} + +export interface PluginConfig { + /** + * A name for the plugin, useful for errors, etc. + */ + name: string; + /** + * A description for the plugin + */ + description?: string; + /** + * Any type definitions we want to add to the schema + */ + schemaTypes?: string; + /** + * Any type definitions we want to add to the type definition option + */ + typeDefTypes?: string; + /** + * Any type definitions we want to add to the field definitions + */ + fieldDefTypes?: string; + /** + * Any extensions to the GraphQLInfoObject (do we need this?) + */ + // infoExtension?: string; + /** + * Any types which should exist as standalone declarations to support this type + */ + localTypes?: string; + /** + * Definition for the plugin. This will be executed against every + * output type field on the schema. + */ + pluginDefinition(pluginInfo: PluginDefinitionInfo): PluginVisitor | void; +} + +/** + * A plugin defines configuration which can document additional metadata options + * for a type definition. + * + * Ultimately everything comes down to the "resolve" function + * which executes the fields, and our plugin system will take that into account. + * + * You can specify options which can be defined on the schema, + * the type or the plugin. The config from each of these will be + * passed in during schema construction time, and used to augment the field as necessary. + * + * You can either return a function, with the new defintion of a resolver implementation, + * or you can return an "enter" / "leave" pairing which will wrap the pre-execution of the + * resolver and the "result" of the resolver, respectively. + */ +export function plugin(config: PluginConfig) { + return new PluginDef(config); +} + +/** + * On "before" plugins, the nextFn allows us to skip to the next resolver + * in the chain + */ +const NEXT_RESULT_VAL = Object.freeze({}); + +/** + * On "after" plugins, the breakerFn allows us to early-return, skipping the + * rest of the plugin stack. Useful when encountering errors + */ +const BREAK_RESULT_VAL = Object.freeze({}); + +/** + * Wraps a resolver with a plugin, "before" the resolver executes. + * Returns a new resolver, which may subsequently be wrapped + */ +export function wrapPluginsBefore( + resolver: GraphQLFieldResolver, + beforePlugins: [string, PluginVisitBefore][] +): GraphQLFieldResolver { + return async (root, args, ctx, info) => { + for (let i = 0; i < beforePlugins.length; i++) { + const [name, before] = beforePlugins[i]; + let result = before(root, args, ctx, info, NEXT_RESULT_VAL); + if (isPromise(result)) { + result = await result; + } + if (result === NEXT_RESULT_VAL) { + continue; + } + if (result === undefined) { + throw new Error( + `Nexus: Expected return value from plugin ${name}:before, saw undefined` + ); + } + return result; + } + return resolver(root, args, ctx, info); + }; +} + +/** + * Wraps a resolver with a plugin, "after" the resolver executes. + * May return a new value for the return type + */ +export function wrapPluginsAfter( + resolver: GraphQLFieldResolver, + afterPlugins: [string, PluginVisitAfter][] +): GraphQLFieldResolver { + return async (root, args, ctx, info) => { + let finalResult: any; + try { + finalResult = resolver(root, args, ctx, info); + if (isPromise(finalResult)) { + finalResult = await finalResult; + } + } catch (e) { + finalResult = e; + } + for (let i = 0; i < afterPlugins.length; i++) { + const [name, after] = afterPlugins[i]; + let returnVal = after( + finalResult, + root, + args, + ctx, + info, + BREAK_RESULT_VAL + ); + if (isPromise(returnVal)) { + returnVal = await returnVal; + } + if (returnVal === BREAK_RESULT_VAL) { + return finalResult; + } + if (returnVal === undefined) { + throw new Error( + `Nexus: Expected return value from plugin ${name}:after, saw undefined` + ); + } + } + return finalResult; + }; +} diff --git a/src/plugins/_all.ts b/src/plugins/_all.ts new file mode 100644 index 00000000..6be8c5ea --- /dev/null +++ b/src/plugins/_all.ts @@ -0,0 +1,6 @@ +export * from "./authorization"; +export * from "./enforceDocs"; +export * from "./handleError"; +export * from "./mockExample"; +export * from "./nullabilityGuard"; +export * from "./validate"; diff --git a/src/plugins/authorization.ts b/src/plugins/authorization.ts new file mode 100644 index 00000000..0b5af1fd --- /dev/null +++ b/src/plugins/authorization.ts @@ -0,0 +1,60 @@ +import { plugin } from "../plugin"; +import { GraphQLFieldResolver } from "graphql"; + +export const AuthorizationPlugin = plugin({ + name: "Authorization", + localTypes: ` +import { core } from 'nexus'; +import { GraphQLResolveInfo } from 'graphql'; +export type AuthorizeResolver< + TypeName extends string, + FieldName extends string +> = ( + root: core.RootValue, + args: core.ArgsValue, + context: core.GetGen<"context">, + info: GraphQLResolveInfo +) => core.PromiseOrValue; + `, + fieldDefTypes: ` +/** + * Authorization for an individual field. Returning "true" + * or "Promise" means the field can be accessed. + * Returning "false" or "Promise" will respond + * with a "Not Authorized" error for the field. Returning + * or throwing an error will also prevent the resolver from + * executing. + */ +authorize?: AuthorizeResolver + `, + pluginDefinition(config) { + if (!config.nexusFieldConfig) { + return; + } + const authorizeFn = config.nexusFieldConfig + .authorize as GraphQLFieldResolver; + if (authorizeFn) { + return { + async before(root, args, ctx, info, next) { + const authResult = await authorizeFn(root, args, ctx, info); + if (authResult === true) { + return next; + } + if (authResult === false) { + throw new Error("Not authorized"); + } + const { + fieldName, + parentType: { name: parentTypeName }, + } = info; + if (authResult === undefined) { + throw new Error( + `Nexus authorize for ${parentTypeName}.${fieldName} Expected a boolean or Error, saw ${authResult}` + ); + } + return next; + }, + }; + } + }, +}); diff --git a/src/plugins/enforceDocs.ts b/src/plugins/enforceDocs.ts new file mode 100644 index 00000000..201d4a5c --- /dev/null +++ b/src/plugins/enforceDocs.ts @@ -0,0 +1,12 @@ +import { plugin } from "../plugin"; + +export const enforceDocs = plugin({ + name: "EnforceDocs", + description: ` + Ensures that any used-defined type must have docs, + otherwise it's a type-error. + `, + typeDefTypes: `description: string;`, + fieldDefTypes: `description: string;`, + pluginDefinition() {}, +}); diff --git a/src/plugins/handleError.ts b/src/plugins/handleError.ts new file mode 100644 index 00000000..efd44e99 --- /dev/null +++ b/src/plugins/handleError.ts @@ -0,0 +1,21 @@ +import { plugin } from "../plugin"; + +export const handleErrorPlugin = plugin({ + name: "HandleError", + description: `Adds error-handling at the root level, so when errors occur they are all logged as necessary.`, + pluginDefinition(config) { + return { + after(result, root, args, ctx, info) { + if (result instanceof Error) { + ctx.logError({ + error: result, + root, + args, + info, + }); + } + return result; + }, + }; + }, +}); diff --git a/src/plugins/mockExample.ts b/src/plugins/mockExample.ts new file mode 100644 index 00000000..50823859 --- /dev/null +++ b/src/plugins/mockExample.ts @@ -0,0 +1,42 @@ +import { plugin } from "../plugin"; + +export const mockResolverPlugin = plugin({ + name: "MockResolver", + description: + "Generates an example resolver that can be used when mocking out a schema", + fieldDefTypes: `mockResolve?: core.FieldResolver`, + schemaTypes: `mockResolversEnabled: boolean | ((ctx: core.GetGen<"context">) => boolean)`, + pluginDefinition(config) { + if (!config.nexusFieldConfig) { + return; + } + const mockResolveFn = config.nexusFieldConfig.mockResolve; + if (!(mockResolveFn instanceof Function)) { + return; + } + const mockResolversEnabled = config.nexusSchemaConfig + .mockResolversEnabled as true | Function; + if (mockResolversEnabled === false) { + return; + } + if ( + mockResolversEnabled !== true && + !(mockResolversEnabled instanceof Function) + ) { + throw new Error( + "Expected the schema config to have a valid mockResolversEnabled property (function or boolean)" + ); + } + return { + before(root, args, ctx, info, nextVal) { + if ( + mockResolversEnabled === true || + mockResolversEnabled(ctx) === true + ) { + return mockResolveFn(root, args, ctx, info); + } + return nextVal; + }, + }; + }, +}); diff --git a/src/plugins/nullabilityGuard.ts b/src/plugins/nullabilityGuard.ts new file mode 100644 index 00000000..309ecab9 --- /dev/null +++ b/src/plugins/nullabilityGuard.ts @@ -0,0 +1,76 @@ +import { plugin } from "../plugin"; +import { isWrappingType, isNonNullType, isListType } from "graphql"; +import { GraphQLNamedOutputType } from "../definitions/_types"; + +export type NullabilityGuardConfig = { + onGuarded?(): void; + fallbackValue?(type: GraphQLNamedOutputType): any; +}; + +export const nullabilityGuard = (pluginConfig: NullabilityGuardConfig) => { + return plugin({ + name: "NullabilityGuard", + description: + "If we have a nullable field, we want to guard against this being an issue in production.", + fieldDefTypes: ` +/** + * The nullability guard can be helpful, but is also a potentially expensive operation for lists. + * We need to iterate the entire list to check for null items to guard against. Set this to + * true to skip the null guard on a specific field if you know there's no potential for unsafe types. + */ +skipNullGuard?: boolean +`, + pluginDefinition(config) { + if ( + config.nexusFieldConfig && + (config.nexusFieldConfig as any).skipNullGuard + ) { + return; + } + let type = config.graphqlFieldConfig.type; + let outerIsList: boolean = false; + let outerNonNull: boolean = false; + let hasListNonNull: boolean = false; + let listNonNull: boolean[] = []; + if (isNonNullType(type)) { + outerNonNull = true; + type = type.ofType; + } + if (isListType(type)) { + outerIsList = true; + type = type.ofType; + } + while (isWrappingType(type)) { + if (isListType(type)) { + type = type.ofType; + } + if (isNonNullType(type)) { + hasListNonNull = true; + listNonNull.push(true); + type = type.ofType; + } else { + listNonNull.push(false); + } + } + if (!outerNonNull && !hasListNonNull) { + return; + } + return { + after(result, root, args, ctx, info, breakVal) { + if (outerNonNull && result == null) { + if (outerIsList) { + return []; + } + } + }, + }; + }, + }); +}; + +export function getFallbackValue( + type: GraphQLNamedOutputType, + config: NullabilityGuardConfig +) { + // +} diff --git a/src/plugins/validate.ts b/src/plugins/validate.ts new file mode 100644 index 00000000..0f09e0c6 --- /dev/null +++ b/src/plugins/validate.ts @@ -0,0 +1,40 @@ +import { plugin } from "../plugin"; + +/** + * This cannot be implemented at the "validate" step, as it requires the variableValues + */ +export const ValidatePlugin = plugin({ + name: "ValidatePlugin", + description: "Validates inputs ahead-of-time, before query execution", + localTypes: ` +type ValidatePluginValidateArgs = () => any + `, + fieldDefTypes: `validateArgs: ValidatePluginValidateArgs`, + pluginDefinition(config) { + const isRootType = + config.typeName === "Query" || + config.typeName === "Mutation" || + config.typeName === "Subscription"; + const fieldValidateArgs = + config.nexusFieldConfig && config.nexusFieldConfig.validateArgs; + // Collect all of the validateArgs in a map so we can validate the input args + // for any fields that have them. + if (fieldValidateArgs instanceof Function) { + config.mutableObj[config.typeName] = + config.mutableObj[config.typeName] || {}; + config.mutableObj[config.typeName][config.fieldName] = fieldValidateArgs; + } + // If we're not on a root type, we don't need to decorate the resolver. + if (!isRootType) { + return; + } + return { + before(root, args, ctx, info, nextVal) { + // Error collector + // const errorObj = {}; + // info.fieldNodes; + return nextVal; + }, + }; + }, +}); diff --git a/src/sdlConverter.ts b/src/sdlConverter.ts index e8112fa6..dc07a068 100644 --- a/src/sdlConverter.ts +++ b/src/sdlConverter.ts @@ -37,6 +37,7 @@ export function convertSDL( try { return new SDLConverter(sdl, commonjs, json).print(); } catch (e) { + /* istanbul ignore next */ return `Error Parsing SDL into Schema: ${e.stack}`; } } diff --git a/src/typegen.ts b/src/typegen.ts index aa2ed666..856a920d 100644 --- a/src/typegen.ts +++ b/src/typegen.ts @@ -8,7 +8,6 @@ import { GraphQLObjectType, GraphQLOutputType, GraphQLScalarType, - GraphQLSchema, GraphQLUnionType, isEnumType, isInputObjectType, @@ -22,9 +21,15 @@ import { GraphQLInputObjectType, GraphQLEnumType, defaultFieldResolver, + GraphQLSchema, } from "graphql"; import path from "path"; -import { TypegenInfo, NexusSchemaExtensions } from "./builder"; +import { + TypegenInfo, + NexusSchemaExtensions, + NexusSchema, + SchemaBuilder, +} from "./builder"; import { eachObj, GroupedTypes, @@ -33,6 +38,8 @@ import { relativePathTo, } from "./utils"; import { WrappedResolver } from "./definitions/_types"; +import { TypegenMetadata } from "./typegenMetadata"; +import { NexusTypeExtensions } from "./definitions/decorateType"; const SpecifiedScalars = { ID: "string", @@ -65,15 +72,24 @@ type RootTypeMapping = Record< * * - Non-scalar types will get a dedicated "Root" type associated with it */ -export class Typegen { +export class TypegenPrinter { + protected sortedSchema: GraphQLSchema; + protected nexusSchema: NexusSchema; + protected builder: SchemaBuilder; + protected typegenFile: string; + protected extensions: NexusSchemaExtensions; groupedTypes: GroupedTypes; constructor( - protected schema: GraphQLSchema, - protected typegenInfo: TypegenInfo & { typegenFile: string }, - protected extensions: NexusSchemaExtensions + protected typegenMetadata: TypegenMetadata, + protected typegenInfo: TypegenInfo ) { - this.groupedTypes = groupTypes(schema); + this.nexusSchema = typegenMetadata.getNexusSchema(); + this.sortedSchema = typegenMetadata.getSortedSchema(); + this.builder = typegenMetadata.getBuilder(); + this.typegenFile = typegenMetadata.getTypegenFile(); + this.extensions = this.nexusSchema.extensions.nexus; + this.groupedTypes = groupTypes(this.sortedSchema); } print() { @@ -94,6 +110,7 @@ export class Typegen { this.printTypeNames("scalar", "NexusGenScalarNames"), this.printTypeNames("union", "NexusGenUnionNames"), this.printGenTypeMap(), + this.printPlugins(), ].join("\n\n"); } @@ -148,7 +165,7 @@ export class Typegen { imports.push(`import { core } from "nexus"`); } const importMap: Record> = {}; - const outputPath = this.typegenInfo.typegenFile; + const outputPath = this.typegenFile; eachObj(rootTypings, (val, key) => { if (typeof val !== "string") { const importPath = (path.isAbsolute(val.path) @@ -236,7 +253,7 @@ export class Typegen { .concat(this.groupedTypes.interface) .forEach((type) => { if (isInterfaceType(type)) { - const possibleNames = this.schema + const possibleNames = this.sortedSchema .getPossibleTypes(type) .map((t) => t.name); if (possibleNames.length > 0) { @@ -269,7 +286,7 @@ export class Typegen { .concat(this.groupedTypes.interface) .forEach((type) => { if (isInterfaceType(type)) { - const possibleNames = this.schema + const possibleNames = this.sortedSchema .getPossibleTypes(type) .map((t) => t.name); if (possibleNames.length > 0) { @@ -367,7 +384,7 @@ export class Typegen { .map((t) => `NexusGenRootTypes['${t.name}']`) .join(" | "); } else if (isInterfaceType(type)) { - const possibleRoots = this.schema + const possibleRoots = this.sortedSchema .getPossibleTypes(type) .map((t) => `NexusGenRootTypes['${t.name}']`); if (possibleRoots.length > 0) { @@ -527,7 +544,11 @@ export class Typegen { if (isListType(type)) { typing.push(this.typeToArr(type.ofType)); } else if (isScalarType(type)) { - typing.push(this.printScalar(type)); + typing.push( + this.printScalar(this.nexusSchema.getType( + type.name + ) as GraphQLScalarType) + ); } else if (isEnumType(type)) { typing.push(`NexusGenEnums['${type.name}']`); } else if ( @@ -667,19 +688,68 @@ export class Typegen { .join("\n"); }; - printScalar(type: GraphQLScalarType) { - if (isSpecifiedScalarType(type)) { - return SpecifiedScalars[type.name as SpecifiedScalarNames]; - } + printScalar(type: GraphQLScalarType & { extensions?: NexusTypeExtensions }) { const backingType = this.typegenInfo.backingTypeMap[type.name]; if (typeof backingType === "string") { return backingType; - } else { - return "any"; } + if (isSpecifiedScalarType(type)) { + return SpecifiedScalars[type.name as SpecifiedScalarNames]; + } + if (type && type.extensions && type.extensions.nexus.rootTyping) { + return type.extensions.nexus.rootTyping; + } + return "any"; + } + + printPlugins() { + const pluginFieldExt: string[] = [ + ` interface NexusGenPluginFieldConfig {`, + ]; + const pluginSchemaExt: string[] = [ + ` interface NexusGenPluginSchemaConfig {`, + ]; + const pluginTypeExt: string[] = [ + ` interface NexusGenPluginTypeConfig {`, + ]; + const printInlineDefs: string[] = []; + const plugins = this.builder.getPlugins(); + plugins.forEach((plugin) => { + if (plugin.config.localTypes) { + printInlineDefs.push(plugin.config.localTypes); + } + if (plugin.config.fieldDefTypes) { + pluginFieldExt.push(padLeft(plugin.config.fieldDefTypes, " ")); + } + if (plugin.config.schemaTypes) { + pluginSchemaExt.push(padLeft(plugin.config.schemaTypes, " ")); + } + if (plugin.config.typeDefTypes) { + pluginTypeExt.push(padLeft(plugin.config.typeDefTypes, " ")); + } + }); + return [ + printInlineDefs.join("\n"), + [ + "declare global {", + [ + pluginTypeExt.concat(" }").join("\n"), + pluginFieldExt.concat(" }").join("\n"), + pluginSchemaExt.concat(" }").join("\n"), + ].join("\n"), + "}", + ].join("\n"), + ].join("\n"); } } +function padLeft(str: string, padding: string) { + return str + .split("\n") + .map((s) => `${padding}${s}`) + .join("\n"); +} + const GLOBAL_DECLARATION = ` declare global { interface NexusGen extends NexusGenTypes {} diff --git a/src/typegenAutoConfig.ts b/src/typegenAutoConfig.ts index 00db055d..05c60015 100644 --- a/src/typegenAutoConfig.ts +++ b/src/typegenAutoConfig.ts @@ -205,7 +205,6 @@ export function typegenAutoConfig(options: TypegenAutoConfigOptions) { } return null; } - const importPath = (path.isAbsolute(pathOrModule) ? relativePathTo(resolvedPath, outputPath) : pathOrModule @@ -352,7 +351,6 @@ function findTypingForFile(absolutePath: string, pathOrModule: string) { // TODO: need to figure out cases where it's a node module // and "typings" is set in the package.json - throw new Error( `Unable to find typings associated with ${pathOrModule}, skipping` ); diff --git a/src/typegenFormatPrettier.ts b/src/typegenFormatPrettier.ts deleted file mode 100644 index 3f138b25..00000000 --- a/src/typegenFormatPrettier.ts +++ /dev/null @@ -1,37 +0,0 @@ -import path from "path"; - -export type TypegenFormatFn = ( - content: string, - type: "types" | "schema" -) => string | Promise; - -export function typegenFormatPrettier( - prettierConfig: string | object -): TypegenFormatFn { - return async function formatTypegen( - content: string, - type: "types" | "schema" - ) { - try { - const prettier = require("prettier") as typeof import("prettier"); - if (typeof prettierConfig === "string") { - if (!path.isAbsolute(prettierConfig)) { - throw new Error( - `Expected prettierrc path to be absolute, saw ${prettierConfig}` - ); - } - const fs = require("fs") as typeof import("fs"); - const util = require("util") as typeof import("util"); - const readFile = util.promisify(fs.readFile); - prettierConfig = JSON.parse(await readFile(prettierConfig, "utf8")); - } - return prettier.format(content, { - ...(prettierConfig as object), - parser: type === "types" ? "typescript" : "graphql", - }); - } catch (e) { - console.error(e); - } - return content; - }; -} diff --git a/src/typegenMetadata.ts b/src/typegenMetadata.ts index 3d7ff216..0b15e955 100644 --- a/src/typegenMetadata.ts +++ b/src/typegenMetadata.ts @@ -1,19 +1,21 @@ -import { GraphQLSchema, lexicographicSortSchema, printSchema } from "graphql"; import path from "path"; -import { Typegen } from "./typegen"; -import { assertAbsolutePath } from "./utils"; +import { Options } from "prettier"; +import { GraphQLSchema, lexicographicSortSchema, printSchema } from "graphql"; +import { TypegenPrinter } from "./typegen"; import { SDL_HEADER, TYPEGEN_HEADER } from "./lang"; import { typegenAutoConfig } from "./typegenAutoConfig"; -import { - typegenFormatPrettier, - TypegenFormatFn, -} from "./typegenFormatPrettier"; import { BuilderConfig, TypegenInfo, NexusSchema, - NexusSchemaExtensions, + SchemaBuilder, } from "./builder"; +import { FileSystem } from "./fileSystem"; + +export type TypegenFormatFn = ( + content: string, + type: "types" | "schema" +) => string | Promise; /** * Passed into the SchemaBuilder, this keeps track of any necessary @@ -21,12 +23,20 @@ import { * generated types and/or SDL artifact, including but not limited to: */ export class TypegenMetadata { + protected config: BuilderConfig; protected typegenFile: string = ""; + protected sortedSchema: GraphQLSchema; + protected finalPrettierConfig?: Options; - constructor(protected config: BuilderConfig) { + constructor( + protected builder: SchemaBuilder, + protected nexusSchema: NexusSchema + ) { + this.sortedSchema = lexicographicSortSchema(nexusSchema); + const config = (this.config = builder.getConfig()); if (config.outputs !== false && config.shouldGenerateArtifacts !== false) { if (config.outputs.typegen) { - this.typegenFile = assertAbsolutePath( + this.typegenFile = this.assertAbsolutePath( config.outputs.typegen, "outputs.typegen" ); @@ -34,117 +44,86 @@ export class TypegenMetadata { } } + getNexusSchema() { + return this.nexusSchema; + } + + getBuilder() { + return this.builder; + } + + getTypegenFile() { + return this.typegenFile; + } + + getSortedSchema() { + return this.sortedSchema; + } + + /** + * Generates the type definitions + */ + protected async generateTypesFile(): Promise { + const typegenInfo = await this.getTypegenInfo(); + return new TypegenPrinter(this, typegenInfo).print(); + } + /** * Generates the artifacts of the build based on what we * know about the schema and how it was defined. */ - async generateArtifacts(schema: NexusSchema) { - const sortedSchema = this.sortSchema(schema); + async generateArtifacts() { if (this.config.outputs) { if (this.config.outputs.schema) { - await this.writeFile( + await this.writeTypeFile( "schema", - this.generateSchemaFile(sortedSchema), + this.generateSchemaFile(this.sortedSchema), this.config.outputs.schema ); } if (this.typegenFile) { - const value = await this.generateTypesFile( - sortedSchema, - schema.extensions.nexus - ); - await this.writeFile("types", value, this.typegenFile); + const value = await this.generateTypesFile(); + await this.writeTypeFile("types", value, this.typegenFile); } } } - sortSchema(schema: GraphQLSchema) { - let sortedSchema = schema; - if (typeof lexicographicSortSchema !== "undefined") { - sortedSchema = lexicographicSortSchema(schema); - } - return sortedSchema; - } - - async writeFile(type: "schema" | "types", output: string, filePath: string) { - if (typeof filePath !== "string" || !path.isAbsolute(filePath)) { - return Promise.reject( - new Error( - `Expected an absolute path to output the GraphQL Nexus ${type}, saw ${filePath}` - ) - ); - } - const fs = require("fs") as typeof import("fs"); - const util = require("util") as typeof import("util"); - const [readFile, writeFile, mkdir] = [ - util.promisify(fs.readFile), - util.promisify(fs.writeFile), - util.promisify(fs.mkdir), - ]; - let formatTypegen: TypegenFormatFn | null = null; - if (typeof this.config.formatTypegen === "function") { - formatTypegen = this.config.formatTypegen; - } else if (this.config.prettierConfig) { - formatTypegen = typegenFormatPrettier(this.config.prettierConfig); - } - const content = - typeof formatTypegen === "function" - ? await formatTypegen(output, type) - : output; + protected async writeTypeFile( + type: "schema" | "types", + output: string, + filePath: string + ) { const [toSave, existing] = await Promise.all([ - content, - readFile(filePath, "utf8").catch(() => ""), + this.formatTypegen(output, type), + FileSystem.getInstance() + .getFile(filePath) + .catch(() => ""), ]); if (toSave !== existing) { - const dirPath = path.dirname(filePath); - try { - await mkdir(dirPath, { recursive: true }); - } catch (e) { - if (e.code !== "EEXIST") { - throw e; - } - } - return writeFile(filePath, toSave); + return FileSystem.getInstance().replaceFile(filePath, toSave); } } /** * Generates the schema, adding any directives as necessary */ - generateSchemaFile(schema: GraphQLSchema): string { + protected generateSchemaFile(schema: GraphQLSchema): string { let printedSchema = printSchema(schema); return [SDL_HEADER, printedSchema].join("\n\n"); } - /** - * Generates the type definitions - */ - async generateTypesFile( - schema: GraphQLSchema, - extensions: NexusSchemaExtensions - ): Promise { - return new Typegen( - schema, - { - ...(await this.getTypegenInfo(schema)), - typegenFile: this.typegenFile, - }, - extensions - ).print(); - } - - async getTypegenInfo(schema: GraphQLSchema): Promise { + async getTypegenInfo(): Promise { if (this.config.typegenConfig) { if (this.config.typegenAutoConfig) { console.warn( `Only one of typegenConfig and typegenAutoConfig should be specified, ignoring typegenConfig` ); } - return this.config.typegenConfig(schema, this.typegenFile); + return this.config.typegenConfig(this.sortedSchema, this.typegenFile); } if (this.config.typegenAutoConfig) { return typegenAutoConfig(this.config.typegenAutoConfig)( - schema, + this.sortedSchema, this.typegenFile ); } @@ -155,4 +134,49 @@ export class TypegenMetadata { backingTypeMap: {}, }; } + + /** + * Takes "content", which is a string representing the GraphQL schema + * or the TypeScript types (as indicated by the "fileType"), and formats them + * with prettier if it is installed. + * @param content + * @param fileType + */ + protected async formatTypegen(content: string, fileType: "types" | "schema") { + if (this.config.formatTypegen instanceof Function) { + return this.config.formatTypegen(content, fileType); + } + if (!this.config.prettierConfig) { + return content; + } + try { + const prettierConfig = this.config.prettierConfig; + const prettier = require("prettier") as typeof import("prettier"); + if (typeof prettierConfig === "string") { + if (!this.finalPrettierConfig) { + this.finalPrettierConfig = JSON.parse( + await FileSystem.getInstance().getFile(prettierConfig) + ) as Options; + } + } else { + this.finalPrettierConfig = prettierConfig; + } + return prettier.format(content, { + ...this.finalPrettierConfig, + parser: fileType === "schema" ? "graphql" : "typescript", + }); + } catch (e) { + console.error(e); + } + return content; + } + + protected assertAbsolutePath(pathName: string, property: string) { + if (!path.isAbsolute(pathName)) { + throw new Error( + `Expected path for ${property} to be a string, saw ${pathName}` + ); + } + return pathName; + } } diff --git a/src/typegenTypeHelpers.ts b/src/typegenTypeHelpers.ts index 743f87a0..44209528 100644 --- a/src/typegenTypeHelpers.ts +++ b/src/typegenTypeHelpers.ts @@ -4,6 +4,12 @@ declare global { interface NexusGen {} interface NexusGenCustomInputMethods {} interface NexusGenCustomOutputMethods {} + interface NexusGenPluginSchemaConfig {} + interface NexusGenPluginTypeConfig {} + interface NexusGenPluginFieldConfig< + TypeName extends string, + FieldName extends string + > {} } export type AllInputTypes = GetGen<"allInputTypes">; @@ -15,33 +21,37 @@ export type FieldType< FieldName extends string > = GetGen3<"fieldTypes", TypeName, FieldName>; -export type MaybePromise = PromiseLike | T; +export type PromiseOrValue = PromiseLike | T; + +export type MaybePromise = PromiseOrValue; + +export type MaybePromiseDeep = PromiseOrValueDeep; /** * Because the GraphQL field execution algorithm automatically * resolves promises at any level of the tree, we use this * to help signify that. */ -export type MaybePromiseDeep = Date extends T - ? MaybePromise +export type PromiseOrValueDeep = Date extends T + ? PromiseOrValue : boolean extends T - ? MaybePromise + ? PromiseOrValue : number extends T - ? MaybePromise + ? PromiseOrValue : string extends T - ? MaybePromise + ? PromiseOrValue : T extends object - ? MaybePromise< + ? PromiseOrValue< | T | { [P in keyof T]: T[P] extends Array - ? MaybePromise>> + ? PromiseOrValue>> : T[P] extends ReadonlyArray - ? MaybePromise>> - : MaybePromiseDeep + ? PromiseOrValue>> + : PromiseOrValueDeep } > - : MaybePromise; + : PromiseOrValue; /** * The NexusAbstractTypeResolver type can be used if you want to preserve type-safety @@ -63,7 +73,7 @@ export interface AbstractTypeResolver { source: RootValue, context: GetGen<"context">, info: GraphQLResolveInfo - ): MaybePromise | null>; + ): PromiseOrValue | null>; } /** @@ -86,8 +96,8 @@ export type FieldResolver = ( context: GetGen<"context">, info: GraphQLResolveInfo ) => - | MaybePromise> - | MaybePromiseDeep>; + | PromiseOrValue> + | PromiseOrValueDeep>; export type SubFieldResolver< TypeName extends string, @@ -99,18 +109,8 @@ export type SubFieldResolver< context: GetGen<"context">, info: GraphQLResolveInfo ) => - | MaybePromise[SubFieldName]> - | MaybePromiseDeep[SubFieldName]>; - -export type AuthorizeResolver< - TypeName extends string, - FieldName extends string -> = ( - root: RootValue, - args: ArgsValue, - context: GetGen<"context">, - info: GraphQLResolveInfo -) => MaybePromise; + | PromiseOrValue[SubFieldName]> + | PromiseOrValueDeep[SubFieldName]>; export type AbstractResolveReturn< TypeName extends string diff --git a/src/utils.ts b/src/utils.ts index d329e795..1b556ac0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,20 +1,22 @@ import { - GraphQLObjectType, + GraphQLArgument, + GraphQLEnumType, + GraphQLFieldConfigArgumentMap, + GraphQLInputObjectType, GraphQLInterfaceType, + GraphQLNamedType, + GraphQLObjectType, + GraphQLScalarType, GraphQLSchema, - GraphQLInputObjectType, GraphQLUnionType, - GraphQLEnumType, - GraphQLScalarType, - isObjectType, + isEnumType, isInputObjectType, + isInterfaceType, + isObjectType, isScalarType, isSpecifiedScalarType, isUnionType, - isInterfaceType, - isEnumType, specifiedScalarTypes, - GraphQLNamedType, } from "graphql"; import path from "path"; import { UNKNOWN_TYPE_SCALAR } from "./builder"; @@ -150,15 +152,6 @@ export function eachObj( export const isObject = (obj: any): boolean => obj !== null && typeof obj === "object"; -export const assertAbsolutePath = (pathName: string, property: string) => { - if (!path.isAbsolute(pathName)) { - throw new Error( - `Expected path for ${property} to be a string, saw ${pathName}` - ); - } - return pathName; -}; - export interface GroupedTypes { input: GraphQLInputObjectType[]; interface: GraphQLInterfaceType[]; @@ -239,3 +232,48 @@ export function relativePathTo( } return path.join(relative, filename); } + +export function argsToConfig( + args: GraphQLArgument[] +): GraphQLFieldConfigArgumentMap { + const result: GraphQLFieldConfigArgumentMap = {}; + args.forEach((arg) => { + const { name, ...argRest } = arg; + result[name] = argRest; + }); + return result; +} + +export type MaybeDeferred = { + resolve: (val?: T) => void; + reject: (err: Error) => void; + value: Promise; +}; + +export type Deferred = { + resolve: (val?: T) => void; + reject: (err: Error) => void; + promise: Promise; +}; + +export function deferred() { + const dfd = {} as Deferred; + dfd.promise = new Promise((_resolve, _reject) => { + dfd.resolve = (val) => { + if (val && val instanceof Error) { + return _reject(val); + } + _resolve(val); + }; + dfd.reject = _reject; + }); + return dfd; +} + +/** + * Container for a possibly-deferred value. + */ +export function maybeDeferred() { + const maybeDfd = {} as MaybeDeferred; + return maybeDfd; +} diff --git a/tests/__snapshots__/backingTypes.spec.ts.snap b/tests/__snapshots__/backingTypes.spec.ts.snap index 938b1daf..c0274f02 100644 --- a/tests/__snapshots__/backingTypes.spec.ts.snap +++ b/tests/__snapshots__/backingTypes.spec.ts.snap @@ -80,5 +80,15 @@ export interface NexusGenTypes { allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; abstractResolveReturn: NexusGenAbstractResolveReturnTypes; +} + + +declare global { + interface NexusGenPluginTypeConfig { + } + interface NexusGenPluginFieldConfig { + } + interface NexusGenPluginSchemaConfig { + } }" `; diff --git a/tests/__snapshots__/dynamicMethod.spec.ts.snap b/tests/__snapshots__/dynamicMethod.spec.ts.snap new file mode 100644 index 00000000..279fc806 --- /dev/null +++ b/tests/__snapshots__/dynamicMethod.spec.ts.snap @@ -0,0 +1,644 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dynamicInputMethod should provide a method on the input definition 1`] = ` +Array [ + "/Users/tgriesser/Github/oss/nexus/nexus/tests/schema.graphql", + "### This file was autogenerated by GraphQL Nexus +### Do not make changes to this file directly + + +\\"\\"\\" +A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the +\`date-time\` format outlined in section 5.6 of the RFC 3339 profile of the ISO +8601 standard for representation of dates and times using the Gregorian calendar. +\\"\\"\\" +scalar DateTime + +type Query { + ok: Boolean! +} + +input SomeInput { + createdAt: DateTime + id: ID + updatedAt: DateTime +} +", +] +`; + +exports[`dynamicInputMethod should provide a method on the input definition 2`] = ` +Array [ + "/Users/tgriesser/Github/oss/nexus/nexus/tests/test-output.ts", + "/** + * This file was automatically generated by GraphQL Nexus + * Do not make changes to this file directly + */ + + +import { core } from \\"nexus\\" +declare global { + interface NexusGenCustomInputMethods { + timestamps(...args: any): void + } +} + + +declare global { + interface NexusGen extends NexusGenTypes {} +} + +export interface NexusGenInputs { + SomeInput: { // input type + createdAt?: Date | null; // DateTime + id?: string | null; // ID + updatedAt?: Date | null; // DateTime + } +} + +export interface NexusGenEnums { +} + +export interface NexusGenRootTypes { + Query: {}; + String: string; + Int: number; + Float: number; + Boolean: boolean; + ID: string; + DateTime: Date; +} + +export interface NexusGenAllTypes extends NexusGenRootTypes { + SomeInput: NexusGenInputs['SomeInput']; +} + +export interface NexusGenFieldTypes { + Query: { // field return type + ok: boolean; // Boolean! + } +} + +export interface NexusGenArgTypes { +} + +export interface NexusGenAbstractResolveReturnTypes { +} + +export interface NexusGenInheritedFields {} + +export type NexusGenObjectNames = \\"Query\\"; + +export type NexusGenInputNames = \\"SomeInput\\"; + +export type NexusGenEnumNames = never; + +export type NexusGenInterfaceNames = never; + +export type NexusGenScalarNames = \\"Boolean\\" | \\"DateTime\\" | \\"Float\\" | \\"ID\\" | \\"Int\\" | \\"String\\"; + +export type NexusGenUnionNames = never; + +export interface NexusGenTypes { + context: any; + inputTypes: NexusGenInputs; + rootTypes: NexusGenRootTypes; + argTypes: NexusGenArgTypes; + fieldTypes: NexusGenFieldTypes; + allTypes: NexusGenAllTypes; + inheritedFields: NexusGenInheritedFields; + objectNames: NexusGenObjectNames; + inputNames: NexusGenInputNames; + enumNames: NexusGenEnumNames; + interfaceNames: NexusGenInterfaceNames; + scalarNames: NexusGenScalarNames; + unionNames: NexusGenUnionNames; + allInputTypes: NexusGenTypes['inputNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['scalarNames']; + allOutputTypes: NexusGenTypes['objectNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['unionNames'] | NexusGenTypes['interfaceNames'] | NexusGenTypes['scalarNames']; + allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] + abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; + abstractResolveReturn: NexusGenAbstractResolveReturnTypes; +} + + +declare global { + interface NexusGenPluginTypeConfig { + } + interface NexusGenPluginFieldConfig { + } + interface NexusGenPluginSchemaConfig { + } +}", +] +`; + +exports[`dynamicOutputMethod CollectionMethod example 1`] = ` +Array [ + "/Users/tgriesser/Github/oss/nexus/nexus/tests/schema.graphql", + "### This file was autogenerated by GraphQL Nexus +### Do not make changes to this file directly + + +type Cat { + id: ID! + name: String! +} + +type CatCollection { + nodes: [Cat!]! + totalCount: Int! +} + +type Query { + cats(page: Int, perPage: Int): CatCollection! +} +", +] +`; + +exports[`dynamicOutputMethod CollectionMethod example 2`] = ` +Array [ + "/Users/tgriesser/Github/oss/nexus/nexus/tests/test-output", + "/** + * This file was automatically generated by GraphQL Nexus + * Do not make changes to this file directly + */ + + +import { core } from \\"nexus\\" + +declare global { + interface NexusGenCustomOutputMethods { + collectionField(fieldName: FieldName, opts: { + type: NexusGenObjectNames | NexusGenInterfaceNames | core.NexusObjectTypeDef | core.NexusInterfaceTypeDef, + nodes: core.SubFieldResolver, + totalCount: core.SubFieldResolver, + args?: core.ArgsRecord, + nullable?: boolean, + description?: string + }): void; + } +} + +declare global { + interface NexusGen extends NexusGenTypes {} +} + +export interface NexusGenInputs { +} + +export interface NexusGenEnums { +} + +export interface NexusGenRootTypes { + Cat: { // root type + id: string; // ID! + name: string; // String! + } + CatCollection: { // root type + nodes: NexusGenRootTypes['Cat'][]; // [Cat!]! + totalCount: number; // Int! + } + Query: {}; + String: string; + Int: number; + Float: number; + Boolean: boolean; + ID: string; +} + +export interface NexusGenAllTypes extends NexusGenRootTypes { +} + +export interface NexusGenFieldTypes { + Cat: { // field return type + id: string; // ID! + name: string; // String! + } + CatCollection: { // field return type + nodes: NexusGenRootTypes['Cat'][]; // [Cat!]! + totalCount: number; // Int! + } + Query: { // field return type + cats: NexusGenRootTypes['CatCollection']; // CatCollection! + } +} + +export interface NexusGenArgTypes { + Query: { + cats: { // args + page?: number | null; // Int + perPage?: number | null; // Int + } + } +} + +export interface NexusGenAbstractResolveReturnTypes { +} + +export interface NexusGenInheritedFields {} + +export type NexusGenObjectNames = \\"Cat\\" | \\"CatCollection\\" | \\"Query\\"; + +export type NexusGenInputNames = never; + +export type NexusGenEnumNames = never; + +export type NexusGenInterfaceNames = never; + +export type NexusGenScalarNames = \\"Boolean\\" | \\"Float\\" | \\"ID\\" | \\"Int\\" | \\"String\\"; + +export type NexusGenUnionNames = never; + +export interface NexusGenTypes { + context: any; + inputTypes: NexusGenInputs; + rootTypes: NexusGenRootTypes; + argTypes: NexusGenArgTypes; + fieldTypes: NexusGenFieldTypes; + allTypes: NexusGenAllTypes; + inheritedFields: NexusGenInheritedFields; + objectNames: NexusGenObjectNames; + inputNames: NexusGenInputNames; + enumNames: NexusGenEnumNames; + interfaceNames: NexusGenInterfaceNames; + scalarNames: NexusGenScalarNames; + unionNames: NexusGenUnionNames; + allInputTypes: NexusGenTypes['inputNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['scalarNames']; + allOutputTypes: NexusGenTypes['objectNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['unionNames'] | NexusGenTypes['interfaceNames'] | NexusGenTypes['scalarNames']; + allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] + abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; + abstractResolveReturn: NexusGenAbstractResolveReturnTypes; +} + + +declare global { + interface NexusGenPluginTypeConfig { + } + interface NexusGenPluginFieldConfig { + } + interface NexusGenPluginSchemaConfig { + } +}", +] +`; + +exports[`dynamicOutputMethod CollectionMethod example 3`] = ` +Object { + "data": Object { + "cats": Object { + "nodes": Array [ + Object { + "id": "Cat:1", + "name": "Felix", + }, + Object { + "id": "Cat:2", + "name": "Booker", + }, + ], + "totalCount": 2, + }, + }, +} +`; + +exports[`dynamicOutputMethod RelayConnectionMethod example 1`] = ` +Array [ + "/Users/tgriesser/Github/oss/nexus/nexus/tests/schema.graphql", + "### This file was autogenerated by GraphQL Nexus +### Do not make changes to this file directly + + +type Cat { + id: ID! + name: String! +} + +type CatEdge { + cursor: ID! + node: Cat! +} + +type CatRelayConnection { + edges: [CatEdge!]! + pageInfo: ConnectionPageInfo! +} + +type ConnectionPageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! +} + +type Query { + cats(after: String, before: String, first: Int, last: Int): CatRelayConnection! +} +", +] +`; + +exports[`dynamicOutputMethod RelayConnectionMethod example 2`] = ` +Array [ + "/Users/tgriesser/Github/oss/nexus/nexus/tests/test-output.ts", + "/** + * This file was automatically generated by GraphQL Nexus + * Do not make changes to this file directly + */ + + +import { core } from \\"nexus\\" + +declare global { + interface NexusGenCustomOutputMethods { + relayConnectionField + (fieldName: FieldName, opts: { + type: NexusGenObjectNames | NexusGenInterfaceNames | core.NexusObjectTypeDef | core.NexusInterfaceTypeDef, + edges: core.SubFieldResolver, + pageInfo: core.SubFieldResolver, + args?: Record>, + nullable?: boolean, + description?: string + }): void + + } +} + +declare global { + interface NexusGen extends NexusGenTypes {} +} + +export interface NexusGenInputs { +} + +export interface NexusGenEnums { +} + +export interface NexusGenRootTypes { + Cat: { // root type + id: string; // ID! + name: string; // String! + } + CatEdge: { // root type + cursor: string; // ID! + node: NexusGenRootTypes['Cat']; // Cat! + } + CatRelayConnection: { // root type + edges: NexusGenRootTypes['CatEdge'][]; // [CatEdge!]! + pageInfo: NexusGenRootTypes['ConnectionPageInfo']; // ConnectionPageInfo! + } + ConnectionPageInfo: { // root type + hasNextPage: boolean; // Boolean! + hasPreviousPage: boolean; // Boolean! + } + Query: {}; + String: string; + Int: number; + Float: number; + Boolean: boolean; + ID: string; +} + +export interface NexusGenAllTypes extends NexusGenRootTypes { +} + +export interface NexusGenFieldTypes { + Cat: { // field return type + id: string; // ID! + name: string; // String! + } + CatEdge: { // field return type + cursor: string; // ID! + node: NexusGenRootTypes['Cat']; // Cat! + } + CatRelayConnection: { // field return type + edges: NexusGenRootTypes['CatEdge'][]; // [CatEdge!]! + pageInfo: NexusGenRootTypes['ConnectionPageInfo']; // ConnectionPageInfo! + } + ConnectionPageInfo: { // field return type + hasNextPage: boolean; // Boolean! + hasPreviousPage: boolean; // Boolean! + } + Query: { // field return type + cats: NexusGenRootTypes['CatRelayConnection']; // CatRelayConnection! + } +} + +export interface NexusGenArgTypes { + Query: { + cats: { // args + after?: string | null; // String + before?: string | null; // String + first?: number | null; // Int + last?: number | null; // Int + } + } +} + +export interface NexusGenAbstractResolveReturnTypes { +} + +export interface NexusGenInheritedFields {} + +export type NexusGenObjectNames = \\"Cat\\" | \\"CatEdge\\" | \\"CatRelayConnection\\" | \\"ConnectionPageInfo\\" | \\"Query\\"; + +export type NexusGenInputNames = never; + +export type NexusGenEnumNames = never; + +export type NexusGenInterfaceNames = never; + +export type NexusGenScalarNames = \\"Boolean\\" | \\"Float\\" | \\"ID\\" | \\"Int\\" | \\"String\\"; + +export type NexusGenUnionNames = never; + +export interface NexusGenTypes { + context: any; + inputTypes: NexusGenInputs; + rootTypes: NexusGenRootTypes; + argTypes: NexusGenArgTypes; + fieldTypes: NexusGenFieldTypes; + allTypes: NexusGenAllTypes; + inheritedFields: NexusGenInheritedFields; + objectNames: NexusGenObjectNames; + inputNames: NexusGenInputNames; + enumNames: NexusGenEnumNames; + interfaceNames: NexusGenInterfaceNames; + scalarNames: NexusGenScalarNames; + unionNames: NexusGenUnionNames; + allInputTypes: NexusGenTypes['inputNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['scalarNames']; + allOutputTypes: NexusGenTypes['objectNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['unionNames'] | NexusGenTypes['interfaceNames'] | NexusGenTypes['scalarNames']; + allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] + abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; + abstractResolveReturn: NexusGenAbstractResolveReturnTypes; +} + + +declare global { + interface NexusGenPluginTypeConfig { + } + interface NexusGenPluginFieldConfig { + } + interface NexusGenPluginSchemaConfig { + } +}", +] +`; + +exports[`dynamicOutputMethod RelayConnectionMethod example 3`] = ` +Object { + "data": Object { + "cats": Object { + "edges": Array [ + Object { + "node": Object { + "id": "Cat:1", + "name": "Felix", + }, + }, + Object { + "node": Object { + "id": "Cat:2", + "name": "Booker", + }, + }, + ], + "pageInfo": Object { + "hasNextPage": false, + "hasPreviousPage": false, + }, + }, + }, +} +`; + +exports[`dynamicOutputProperty should provide a way for adding a chainable api on the output definition 1`] = ` +Array [ + "/Users/tgriesser/Github/oss/nexus/nexus/tests/schema.graphql", + "### This file was autogenerated by GraphQL Nexus +### Do not make changes to this file directly + + +\\"\\"\\" +A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the +\`date-time\` format outlined in section 5.6 of the RFC 3339 profile of the ISO +8601 standard for representation of dates and times using the Gregorian calendar. +\\"\\"\\" +scalar DateTime + +type DynamicPropObject { + createdAt: DateTime! + id: ID! + updatedAt: DateTime! +} + +type Query { + ok: Boolean! +} +", +] +`; + +exports[`dynamicOutputProperty should provide a way for adding a chainable api on the output definition 2`] = ` +Array [ + "/Users/tgriesser/Github/oss/nexus/nexus/tests/test-output.ts", + "/** + * This file was automatically generated by GraphQL Nexus + * Do not make changes to this file directly + */ + + + + + + +declare global { + interface NexusGen extends NexusGenTypes {} +} + +export interface NexusGenInputs { +} + +export interface NexusGenEnums { +} + +export interface NexusGenRootTypes { + DynamicPropObject: { // root type + createdAt: Date; // DateTime! + id: string; // ID! + updatedAt: Date; // DateTime! + } + Query: {}; + String: string; + Int: number; + Float: number; + Boolean: boolean; + ID: string; + DateTime: Date; +} + +export interface NexusGenAllTypes extends NexusGenRootTypes { +} + +export interface NexusGenFieldTypes { + DynamicPropObject: { // field return type + createdAt: Date; // DateTime! + id: string; // ID! + updatedAt: Date; // DateTime! + } + Query: { // field return type + ok: boolean; // Boolean! + } +} + +export interface NexusGenArgTypes { +} + +export interface NexusGenAbstractResolveReturnTypes { +} + +export interface NexusGenInheritedFields {} + +export type NexusGenObjectNames = \\"DynamicPropObject\\" | \\"Query\\"; + +export type NexusGenInputNames = never; + +export type NexusGenEnumNames = never; + +export type NexusGenInterfaceNames = never; + +export type NexusGenScalarNames = \\"Boolean\\" | \\"DateTime\\" | \\"Float\\" | \\"ID\\" | \\"Int\\" | \\"String\\"; + +export type NexusGenUnionNames = never; + +export interface NexusGenTypes { + context: any; + inputTypes: NexusGenInputs; + rootTypes: NexusGenRootTypes; + argTypes: NexusGenArgTypes; + fieldTypes: NexusGenFieldTypes; + allTypes: NexusGenAllTypes; + inheritedFields: NexusGenInheritedFields; + objectNames: NexusGenObjectNames; + inputNames: NexusGenInputNames; + enumNames: NexusGenEnumNames; + interfaceNames: NexusGenInterfaceNames; + scalarNames: NexusGenScalarNames; + unionNames: NexusGenUnionNames; + allInputTypes: NexusGenTypes['inputNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['scalarNames']; + allOutputTypes: NexusGenTypes['objectNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['unionNames'] | NexusGenTypes['interfaceNames'] | NexusGenTypes['scalarNames']; + allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] + abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; + abstractResolveReturn: NexusGenAbstractResolveReturnTypes; +} + + +declare global { + interface NexusGenPluginTypeConfig { + } + interface NexusGenPluginFieldConfig { + } + interface NexusGenPluginSchemaConfig { + } +}", +] +`; diff --git a/tests/__snapshots__/interfaceType.spec.ts.snap b/tests/__snapshots__/interfaceType.spec.ts.snap new file mode 100644 index 00000000..15d7cbcf --- /dev/null +++ b/tests/__snapshots__/interfaceType.spec.ts.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`interfaceType can be implemented by object types 1`] = ` +Array [ + "/Users/tgriesser/Github/oss/nexus/nexus/tests/interfaceTypeTest.graphql", + "### This file was autogenerated by GraphQL Nexus +### Do not make changes to this file directly + + +interface Node { + id: ID! +} + +type Query { + user: User! +} + +type User implements Node { + id: ID! + name: String! +} +", +] +`; + +exports[`interfaceType can be implemented by object types 2`] = ` +Object { + "data": Object { + "user": Object { + "id": "User:1", + "name": "Test User", + }, + }, +} +`; diff --git a/tests/__snapshots__/typegen.spec.ts.snap b/tests/__snapshots__/typegen.spec.ts.snap index 83e531d5..2d5164a2 100644 --- a/tests/__snapshots__/typegen.spec.ts.snap +++ b/tests/__snapshots__/typegen.spec.ts.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`typegen builds the enum object type defs 1`] = ` +exports[`TypegenPrinter builds the enum object type defs 1`] = ` "export interface NexusGenEnums { OrderEnum: \\"ASC\\" | \\"DESC\\" SomeEnum: \\"A\\" | \\"B\\" }" `; -exports[`typegen builds the input object type defs 1`] = ` +exports[`TypegenPrinter builds the input object type defs 1`] = ` "export interface NexusGenInputs { CreatePostInput: { // input type author: string; // ID! @@ -21,7 +21,7 @@ exports[`typegen builds the input object type defs 1`] = ` }" `; -exports[`typegen should build an argument type map 1`] = ` +exports[`TypegenPrinter should build an argument type map 1`] = ` "export interface NexusGenArgTypes { Mutation: { createPost: { // args @@ -50,7 +50,7 @@ exports[`typegen should build an argument type map 1`] = ` }" `; -exports[`typegen should not print roots for fields with resolvers 1`] = ` +exports[`TypegenPrinter should not print roots for fields with resolvers 1`] = ` "export interface NexusGenRootTypes { Mutation: {}; Post: { // root type @@ -74,12 +74,14 @@ exports[`typegen should not print roots for fields with resolvers 1`] = ` Float: number; Boolean: boolean; ID: string; + CustomScalarMethod: any; + DecoratedCustomScalar: any; UUID: string; ExampleUnion: NexusGenRootTypes['Post'] | NexusGenRootTypes['User']; }" `; -exports[`typegen should print a return type map 1`] = ` +exports[`TypegenPrinter should print a return type map 1`] = ` "export interface NexusGenFieldTypes { Mutation: { // field return type createPost: NexusGenRootTypes['Post']; // Post! @@ -112,7 +114,7 @@ exports[`typegen should print a return type map 1`] = ` }" `; -exports[`typegen should print a root type map 1`] = ` +exports[`TypegenPrinter should print a root type map 1`] = ` "export interface NexusGenRootTypes { Mutation: {}; Post: { // root type @@ -137,21 +139,33 @@ exports[`typegen should print a root type map 1`] = ` Float: number; Boolean: boolean; ID: string; + CustomScalarMethod: any; + DecoratedCustomScalar: any; UUID: string; ExampleUnion: NexusGenRootTypes['Post'] | NexusGenRootTypes['User']; }" `; -exports[`typegen should print the full output 1`] = ` +exports[`TypegenPrinter should print the full output 1`] = ` "/** * This file was automatically generated by GraphQL Nexus * Do not make changes to this file directly */ import * as t from \\"./_helpers\\" - - - +import { core } from \\"nexus\\" +declare global { + interface NexusGenCustomInputMethods { + CustomScalarMethod(fieldName: FieldName, opts?: core.ScalarInputFieldConfig>): void // \\"CustomScalarMethod\\"; + DecoratedCustomScalar(fieldName: FieldName, opts?: core.ScalarInputFieldConfig>): void // \\"DecoratedCustomScalar\\"; + } +} +declare global { + interface NexusGenCustomOutputMethods { + CustomScalarMethod(fieldName: FieldName, ...opts: core.ScalarOutSpread): void // \\"CustomScalarMethod\\"; + DecoratedCustomScalar(fieldName: FieldName, ...opts: core.ScalarOutSpread): void // \\"DecoratedCustomScalar\\"; + } +} declare global { interface NexusGen extends NexusGenTypes {} @@ -198,6 +212,8 @@ export interface NexusGenRootTypes { Float: number; Boolean: boolean; ID: string; + CustomScalarMethod: any; + DecoratedCustomScalar: any; UUID: string; ExampleUnion: NexusGenRootTypes['Post'] | NexusGenRootTypes['User']; } @@ -282,7 +298,7 @@ export type NexusGenEnumNames = \\"OrderEnum\\" | \\"SomeEnum\\"; export type NexusGenInterfaceNames = \\"Node\\"; -export type NexusGenScalarNames = \\"Boolean\\" | \\"Float\\" | \\"ID\\" | \\"Int\\" | \\"String\\" | \\"UUID\\"; +export type NexusGenScalarNames = \\"Boolean\\" | \\"CustomScalarMethod\\" | \\"DecoratedCustomScalar\\" | \\"Float\\" | \\"ID\\" | \\"Int\\" | \\"String\\" | \\"UUID\\"; export type NexusGenUnionNames = \\"ExampleUnion\\"; @@ -305,5 +321,15 @@ export interface NexusGenTypes { allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; abstractResolveReturn: NexusGenAbstractResolveReturnTypes; +} + + +declare global { + interface NexusGenPluginTypeConfig { + } + interface NexusGenPluginFieldConfig { + } + interface NexusGenPluginSchemaConfig { + } }" `; diff --git a/tests/__snapshots__/unionType.spec.ts.snap b/tests/__snapshots__/unionType.spec.ts.snap new file mode 100644 index 00000000..652980a0 --- /dev/null +++ b/tests/__snapshots__/unionType.spec.ts.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`unionType unionType 1`] = ` +Array [ + "/Users/tgriesser/Github/oss/nexus/nexus/tests/unionTypeTest.graphql", + "### This file was autogenerated by GraphQL Nexus +### Do not make changes to this file directly + + +type DeletedUser { + message: String! +} + +type Query { + deletedUserTest: UserOrError! + userTest: UserOrError! +} + +type User { + id: Int! + name: String! +} + +union UserOrError = DeletedUser | User +", +] +`; + +exports[`unionType unionType 2`] = ` +Object { + "data": Object { + "deletedUserTest": Object { + "__typename": "DeletedUser", + "message": "This user 1 was deleted", + }, + "userTest": Object { + "__typename": "User", + "id": 1, + "name": "Test User", + }, + }, +} +`; diff --git a/tests/__snapshots__/unknownType.spec.ts.snap b/tests/__snapshots__/unknownType.spec.ts.snap new file mode 100644 index 00000000..465073f9 --- /dev/null +++ b/tests/__snapshots__/unknownType.spec.ts.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`unknownType should render the typegen but throw in the next-tick 1`] = ` +Array [ + "/Users/tgriesser/Github/oss/nexus/nexus/tests/test.graphql", + "### This file was autogenerated by GraphQL Nexus +### Do not make changes to this file directly + + +type CustomUserName { + id: ID! + name: String! +} + +\\"\\"\\" +This scalar should never make it into production. It is used as a placeholder +for situations where GraphQL Nexus encounters a missing type. We don't want to +error immedately, otherwise the TypeScript definitions will not be updated. +\\"\\"\\" +scalar NEXUS__UNKNOWN__TYPE + +type Query { + user: CustomUserName! + user2: NEXUS__UNKNOWN__TYPE! +} +", +] +`; + +exports[`unknownType should render the typegen but throw in the next-tick 2`] = ` +Array [ + "/Users/tgriesser/Github/oss/nexus/nexus/tests/test.ts", + "/** + * This file was automatically generated by GraphQL Nexus + * Do not make changes to this file directly + */ + + + + + + +declare global { + interface NexusGen extends NexusGenTypes {} +} + +export interface NexusGenInputs { +} + +export interface NexusGenEnums { +} + +export interface NexusGenRootTypes { + CustomUserName: { // root type + id: string; // ID! + name: string; // String! + } + Query: {}; + String: string; + Int: number; + Float: number; + Boolean: boolean; + ID: string; +} + +export interface NexusGenAllTypes extends NexusGenRootTypes { +} + +export interface NexusGenFieldTypes { + CustomUserName: { // field return type + id: string; // ID! + name: string; // String! + } + Query: { // field return type + user: NexusGenRootTypes['CustomUserName']; // CustomUserName! + user2: never; // NEXUS__UNKNOWN__TYPE! + } +} + +export interface NexusGenArgTypes { +} + +export interface NexusGenAbstractResolveReturnTypes { +} + +export interface NexusGenInheritedFields {} + +export type NexusGenObjectNames = \\"CustomUserName\\" | \\"Query\\"; + +export type NexusGenInputNames = never; + +export type NexusGenEnumNames = never; + +export type NexusGenInterfaceNames = never; + +export type NexusGenScalarNames = \\"Boolean\\" | \\"Float\\" | \\"ID\\" | \\"Int\\" | \\"String\\"; + +export type NexusGenUnionNames = never; + +export interface NexusGenTypes { + context: any; + inputTypes: NexusGenInputs; + rootTypes: NexusGenRootTypes; + argTypes: NexusGenArgTypes; + fieldTypes: NexusGenFieldTypes; + allTypes: NexusGenAllTypes; + inheritedFields: NexusGenInheritedFields; + objectNames: NexusGenObjectNames; + inputNames: NexusGenInputNames; + enumNames: NexusGenEnumNames; + interfaceNames: NexusGenInterfaceNames; + scalarNames: NexusGenScalarNames; + unionNames: NexusGenUnionNames; + allInputTypes: NexusGenTypes['inputNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['scalarNames']; + allOutputTypes: NexusGenTypes['objectNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['unionNames'] | NexusGenTypes['interfaceNames'] | NexusGenTypes['scalarNames']; + allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] + abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; + abstractResolveReturn: NexusGenAbstractResolveReturnTypes; +} + + +declare global { + interface NexusGenPluginTypeConfig { + } + interface NexusGenPluginFieldConfig { + } + interface NexusGenPluginSchemaConfig { + } +}", +] +`; diff --git a/tests/_fixtures.ts b/tests/_fixtures.ts new file mode 100644 index 00000000..34246e0c --- /dev/null +++ b/tests/_fixtures.ts @@ -0,0 +1,4 @@ +export const CatListFixture = [ + { id: "Cat:1", name: "Felix" }, + { id: "Cat:2", name: "Booker" }, +]; diff --git a/tests/backingTypes.spec.ts b/tests/backingTypes.spec.ts index c9fb911f..edf4b79c 100644 --- a/tests/backingTypes.spec.ts +++ b/tests/backingTypes.spec.ts @@ -1,8 +1,9 @@ import path from "path"; -import { core, makeSchema, queryType, enumType } from "../src"; +import { core, queryType, enumType } from "../src"; import { A, B } from "./_types"; +import { makeSchemaInternal } from "../src/core"; -const { Typegen, TypegenMetadata } = core; +const { TypegenPrinter, TypegenMetadata } = core; export enum TestEnum { A = "a", @@ -10,7 +11,7 @@ export enum TestEnum { } function getSchemaWithNormalEnums() { - return makeSchema({ + return makeSchemaInternal({ types: [ enumType({ name: "A", @@ -27,7 +28,7 @@ function getSchemaWithNormalEnums() { } function getSchemaWithConstEnums() { - return makeSchema({ + return core.makeSchemaInternal({ types: [ enumType({ name: "B", @@ -47,7 +48,8 @@ describe("backingTypes", () => { let metadata: core.TypegenMetadata; beforeEach(async () => { - metadata = new TypegenMetadata({ + const { builder, schema } = core.makeSchemaInternal({ + types: [], outputs: { typegen: path.join(__dirname, "test-gen.ts"), schema: path.join(__dirname, "test-gen.graphql"), @@ -62,36 +64,34 @@ describe("backingTypes", () => { contextType: "t.TestContext", }, }); + metadata = new TypegenMetadata(builder, schema); }); it("can match backing types to regular enums", async () => { - const schema = getSchemaWithNormalEnums(); - const typegenInfo = await metadata.getTypegenInfo(schema); - const typegen = new Typegen( - schema, - { ...typegenInfo, typegenFile: "" }, - (schema as any).extensions.nexus + const { schema, builder } = getSchemaWithNormalEnums(); + const metadata = new TypegenMetadata(builder, schema); + const typegen = new TypegenPrinter( + metadata, + await metadata.getTypegenInfo() ); expect(typegen.printEnumTypeMap()).toMatchInlineSnapshot(` "export interface NexusGenEnums { - A: t.A + A: \\"ONE\\" | \\"TWO\\" }" `); }); it("can match backing types for const enums", async () => { - const schema = getSchemaWithConstEnums(); - const typegenInfo = await metadata.getTypegenInfo(schema); - const typegen = new Typegen( - schema, - { ...typegenInfo, typegenFile: "" }, - (schema as any).extensions.nexus + const { schema, builder } = getSchemaWithConstEnums(); + const metadata = new TypegenMetadata(builder, schema); + const typegen = new TypegenPrinter( + metadata, + await metadata.getTypegenInfo() ); - expect(typegen.printEnumTypeMap()).toMatchInlineSnapshot(` "export interface NexusGenEnums { - B: t.B + B: \\"9\\" | \\"10\\" }" `); }); @@ -99,8 +99,7 @@ describe("backingTypes", () => { describe("rootTypings", () => { it("can import enum via rootTyping", async () => { - const metadata = new TypegenMetadata({ outputs: false }); - const schema = makeSchema({ + const { schema, builder } = makeSchemaInternal({ types: [ enumType({ name: "TestEnumType", @@ -113,11 +112,10 @@ describe("rootTypings", () => { ], outputs: false, }); - const typegenInfo = await metadata.getTypegenInfo(schema); - const typegen = new Typegen( - schema, - { ...typegenInfo, typegenFile: "" }, - (schema as any).extensions.nexus + const metadata = new TypegenMetadata(builder, schema); + const typegen = new TypegenPrinter( + metadata, + await metadata.getTypegenInfo() ); expect(typegen.print()).toMatchSnapshot(); }); diff --git a/tests/builder.spec.ts b/tests/builder.spec.ts new file mode 100644 index 00000000..844b26b0 --- /dev/null +++ b/tests/builder.spec.ts @@ -0,0 +1,63 @@ +import { FileSystem } from "../src/fileSystem"; +import { + SchemaBuilder, + makeSchema, + unionType, + interfaceType, + objectType, +} from "../src/core"; + +describe("SchemaBuilder", () => { + let errSpy: jest.SpyInstance; + beforeEach(() => { + jest.clearAllMocks(); + errSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + test("missingResolveType is called when a union type is missing a resolveType", async () => { + const spy = jest.spyOn(SchemaBuilder.prototype, "missingResolveType"); + makeSchema({ + types: [ + unionType({ + name: "TestUnion", + definition(t) { + t.members( + objectType({ + name: "Test", + definition(t) { + t.string("foo"); + }, + }) + ); + }, + }), + ], + outputs: false, + }); + expect(spy).toBeCalledTimes(1); + expect(errSpy).toBeCalledTimes(1); + const resolvedVal = spy.mock.results[0].value; + expect(typeof resolvedVal).toEqual("function"); + expect(resolvedVal()).toEqual(null); + }); + + test("missingResolveType is called when an interface type is missing a resolveType", async () => { + const spy = jest.spyOn(SchemaBuilder.prototype, "missingResolveType"); + makeSchema({ + types: [ + interfaceType({ + name: "Node", + definition(t) { + t.id("id"); + }, + }), + ], + outputs: false, + }); + expect(spy).toBeCalledTimes(1); + expect(errSpy).toBeCalledTimes(1); + const resolvedVal = spy.mock.results[0].value; + expect(typeof resolvedVal).toEqual("function"); + expect(resolvedVal()).toEqual(null); + }); +}); diff --git a/tests/definitions.spec.ts b/tests/definitions.spec.ts index 1aa300c5..b961459c 100644 --- a/tests/definitions.spec.ts +++ b/tests/definitions.spec.ts @@ -1,8 +1,12 @@ /// -import { GraphQLEnumType, GraphQLObjectType, printType } from "graphql"; +import { + GraphQLEnumType, + GraphQLObjectType, + printType, + GraphQLNamedType, +} from "graphql"; import { idArg, - buildTypes, enumType, extendType, objectType, @@ -10,6 +14,7 @@ import { extendInputType, } from "../src"; import { UserObject, PostObject } from "./_helpers"; +import { SchemaBuilder } from "../src/core"; enum NativeColors { RED = "RED", @@ -23,6 +28,10 @@ enum NativeNumbers { THREE = 3, } +function buildTypes>(types: any) { + return new SchemaBuilder({ outputs: false, types }).getFinalTypeMap(); +} + describe("enumType", () => { it("builds an enum", () => { const PrimaryColors = enumType({ diff --git a/tests/dynamicMethod.spec.ts b/tests/dynamicMethod.spec.ts new file mode 100644 index 00000000..7ba7560d --- /dev/null +++ b/tests/dynamicMethod.spec.ts @@ -0,0 +1,254 @@ +import path from "path"; +import { GraphQLDateTime } from "graphql-iso-date"; +import { + makeSchema, + objectType, + queryType, + deferred, + inputObjectType, + dynamicInputMethod, + decorateType, +} from "../src/core"; +import { RelayConnectionMethod, CollectionMethod } from "../src/extensions"; +import { FileSystem } from "../src/fileSystem"; +import { graphql } from "graphql"; +import { CatListFixture } from "./_fixtures"; +import { dynamicOutputProperty } from "../src/dynamicProperty"; + +let spy: jest.SpyInstance; +beforeEach(() => { + jest.clearAllMocks(); + spy = jest + .spyOn(FileSystem.prototype, "replaceFile") + .mockImplementation(async () => null); +}); + +describe("dynamicOutputMethod", () => { + const Cat = objectType({ + name: "Cat", + definition(t) { + t.id("id"); + t.string("name"); + }, + }); + + test("RelayConnectionMethod example", async () => { + const dfd = deferred(); + const Query = queryType({ + definition(t) { + // @ts-ignore + t.relayConnectionField("cats", { + type: Cat, + pageInfo: () => ({ + hasNextPage: false, + hasPreviousPage: false, + }), + edges: () => + CatListFixture.map((c) => ({ cursor: `Cursor: ${c.id}`, node: c })), + }); + }, + }); + const schema = makeSchema({ + types: [Query, RelayConnectionMethod], + outputs: { + typegen: path.join(__dirname, "test-output.ts"), + schema: path.join(__dirname, "schema.graphql"), + }, + shouldGenerateArtifacts: true, + onReady: dfd.resolve, + }); + await dfd.promise; + expect(spy.mock.calls[0]).toMatchSnapshot(); + expect(spy.mock.calls[1]).toMatchSnapshot(); + expect( + await graphql( + schema, + ` + { + cats { + edges { + node { + id + name + } + } + pageInfo { + hasNextPage + hasPreviousPage + } + } + } + ` + ) + ).toMatchSnapshot(); + }); + + test("CollectionMethod example", async () => { + const dynamicOutputMethod = queryType({ + definition(t) { + // @ts-ignore + t.collectionField("cats", { + type: Cat, + totalCount: () => CatListFixture.length, + nodes: () => CatListFixture, + }); + }, + }); + + const dfd = deferred(); + + const schema = makeSchema({ + types: [dynamicOutputMethod, CollectionMethod], + outputs: { + typegen: path.join(__dirname, "test-output"), + schema: path.join(__dirname, "schema.graphql"), + }, + onReady: dfd.resolve, + shouldGenerateArtifacts: true, + }); + + await dfd.promise; + expect(spy.mock.calls[0]).toMatchSnapshot(); + expect(spy.mock.calls[1]).toMatchSnapshot(); + + expect( + await graphql( + schema, + ` + { + cats { + totalCount + nodes { + id + name + } + } + } + ` + ) + ).toMatchSnapshot(); + }); + + test("CollectionMethod example with string type ref", () => { + makeSchema({ + types: [ + queryType({ + definition(t) { + // @ts-ignore + t.collectionField("cats", { + type: "Cat", + totalCount: () => CatListFixture.length, + nodes: () => CatListFixture, + }); + }, + }), + CollectionMethod, + ], + outputs: false, + }); + }); + + test("RelayConnectionMethod example with string type ref", async () => { + makeSchema({ + types: [ + queryType({ + definition(t) { + // @ts-ignore + t.relayConnectionField("cats", { + type: "Cat", + pageInfo: () => ({ + hasNextPage: false, + hasPreviousPage: false, + }), + edges: () => + CatListFixture.map((c) => ({ + cursor: `Cursor: ${c.id}`, + node: c, + })), + }); + }, + }), + RelayConnectionMethod, + ], + outputs: false, + }); + }); +}); + +describe("dynamicInputMethod", () => { + it("should provide a method on the input definition", async () => { + const dfd = deferred(); + makeSchema({ + types: [ + decorateType(GraphQLDateTime, { + rootTyping: "Date", + }), + inputObjectType({ + name: "SomeInput", + definition(t) { + t.id("id"); + // @ts-ignore + t.timestamps(); + }, + }), + dynamicInputMethod({ + name: "timestamps", + factory({ typeDef }) { + typeDef.field("createdAt", { type: "DateTime" }); + typeDef.field("updatedAt", { type: "DateTime" }); + }, + }), + ], + outputs: { + typegen: path.join(__dirname, "test-output.ts"), + schema: path.join(__dirname, "schema.graphql"), + }, + onReady: dfd.resolve, + shouldGenerateArtifacts: true, + }); + await dfd.promise; + expect(spy.mock.calls[0]).toMatchSnapshot(); + expect(spy.mock.calls[1]).toMatchSnapshot(); + }); +}); + +describe("dynamicOutputProperty", () => { + it("should provide a way for adding a chainable api on the output definition", async () => { + const dfd = deferred(); + makeSchema({ + types: [ + decorateType(GraphQLDateTime, { + rootTyping: "Date", + }), + objectType({ + name: "DynamicPropObject", + definition(t) { + t.id("id"); + // @ts-ignore + t.model.timestamps(); + }, + }), + dynamicOutputProperty({ + name: "model", + factory({ typeDef }) { + return { + timestamps() { + typeDef.field("createdAt", { type: "DateTime" }); + typeDef.field("updatedAt", { type: "DateTime" }); + }, + }; + }, + }), + ], + outputs: { + typegen: path.join(__dirname, "test-output.ts"), + schema: path.join(__dirname, "schema.graphql"), + }, + onReady: dfd.resolve, + shouldGenerateArtifacts: true, + }); + await dfd.promise; + expect(spy.mock.calls[0]).toMatchSnapshot(); + expect(spy.mock.calls[1]).toMatchSnapshot(); + }); +}); diff --git a/tests/interfaceType.spec.ts b/tests/interfaceType.spec.ts new file mode 100644 index 00000000..03cc2b47 --- /dev/null +++ b/tests/interfaceType.spec.ts @@ -0,0 +1,68 @@ +import { graphql } from "graphql"; +import path from "path"; +import { + deferred, + interfaceType, + makeSchema, + objectType, + queryField, +} from "../src/core"; +import { FileSystem } from "../src/fileSystem"; + +describe("interfaceType", () => { + let spy: jest.SpyInstance; + beforeEach(() => { + jest.clearAllMocks(); + spy = jest + .spyOn(FileSystem.prototype, "replaceFile") + .mockImplementation(async () => null); + }); + + it("can be implemented by object types", async () => { + const dfd = deferred(); + const schema = makeSchema({ + types: [ + interfaceType({ + name: "Node", + definition(t) { + t.id("id"); + t.resolveType(() => null); + }, + }), + objectType({ + name: "User", + definition(t) { + t.implements("Node"); + t.string("name"); + }, + }), + queryField("user", { + type: "User", + resolve: () => ({ id: `User:1`, name: "Test User" }), + }), + ], + onReady: dfd.resolve, + outputs: { + schema: path.join(__dirname, "interfaceTypeTest.graphql"), + typegen: false, + }, + shouldGenerateArtifacts: true, + }); + await dfd.promise; + expect(spy).toBeCalledTimes(1); + expect(spy.mock.calls[0]).toMatchSnapshot(); + expect( + await graphql( + schema, + ` + { + user { + id + name + } + } + ` + ) + ).toMatchSnapshot(); + }); +}); diff --git a/tests/mutationField.spec.ts b/tests/mutationField.spec.ts new file mode 100644 index 00000000..fd0613ed --- /dev/null +++ b/tests/mutationField.spec.ts @@ -0,0 +1,26 @@ +import { makeSchema, mutationField } from "../src/core"; + +describe("mutationField", () => { + it("defines a field on the mutation type as shorthand", () => { + makeSchema({ + types: [ + mutationField("someField", { + type: "String", + resolve: () => "Hello World", + }), + ], + outputs: false, + }); + }); + it("can be defined as a thunk", () => { + makeSchema({ + types: [ + mutationField("someField", () => ({ + type: "String", + resolve: () => "Hello World", + })), + ], + outputs: false, + }); + }); +}); diff --git a/tests/plugins/authorization.spec.ts b/tests/plugins/authorization.spec.ts new file mode 100644 index 00000000..d4e09049 --- /dev/null +++ b/tests/plugins/authorization.spec.ts @@ -0,0 +1,14 @@ +import { makeSchema, queryField } from "../.."; + +describe("plugin: authorization", () => { + it('adds an "authorize" property to the field definitions', () => { + const schema = makeSchema({ + types: [ + queryField("someField", { + type: "String", + authorize: () => new Error("Not Authorized"), + }), + ], + }); + }); +}); diff --git a/tests/queryField.spec.ts b/tests/queryField.spec.ts new file mode 100644 index 00000000..c05d7ee2 --- /dev/null +++ b/tests/queryField.spec.ts @@ -0,0 +1,26 @@ +import { makeSchema, queryField } from "../src/core"; + +describe("queryField", () => { + it("defines a field on the query type as shorthand", () => { + makeSchema({ + types: [ + queryField("someField", { + type: "String", + resolve: () => "Hello World", + }), + ], + outputs: false, + }); + }); + it("can be defined as a thunk", () => { + makeSchema({ + types: [ + queryField("someField", () => ({ + type: "String", + resolve: () => "Hello World", + })), + ], + outputs: false, + }); + }); +}); diff --git a/tests/subscriptionField.spec.ts b/tests/subscriptionField.spec.ts new file mode 100644 index 00000000..dc6395cc --- /dev/null +++ b/tests/subscriptionField.spec.ts @@ -0,0 +1,42 @@ +import { makeSchema, subscriptionField } from "../src/core"; + +describe("subscriptionField", () => { + it("defines a field on the mutation type as shorthand", () => { + makeSchema({ + types: [ + subscriptionField("someField", { + type: "String", + async subscribe() { + let val = 0; + return { + next() { + return `Num:${val++}`; + }, + }; + }, + resolve: () => "Hello World", + }), + ], + outputs: false, + }); + }); + it("can be defined as a thunk", () => { + makeSchema({ + types: [ + subscriptionField("someField", () => ({ + type: "String", + async subscribe() { + let val = 0; + return { + next() { + return `Num:${val++}`; + }, + }; + }, + resolve: () => "Hello World", + })), + ], + outputs: false, + }); + }); +}); diff --git a/tests/typegen.spec.ts b/tests/typegen.spec.ts index 334bfe85..45312c5d 100644 --- a/tests/typegen.spec.ts +++ b/tests/typegen.spec.ts @@ -4,19 +4,21 @@ import { GraphQLField, GraphQLObjectType, GraphQLInterfaceType, + GraphQLScalarType, } from "graphql"; import path from "path"; import { core } from "../src"; import { EXAMPLE_SDL } from "./_sdl"; +import { makeSchemaInternal, scalarType, decorateType } from "../src/core"; -const { Typegen, TypegenMetadata } = core; +const { TypegenMetadata, TypegenPrinter } = core; -describe("typegen", () => { - let typegen: core.Typegen; +describe("TypegenPrinter", () => { let metadata: core.TypegenMetadata; + let typegen: core.TypegenPrinter; beforeEach(async () => { const schema = lexicographicSortSchema(buildSchema(EXAMPLE_SDL)); - metadata = new TypegenMetadata({ + const builder = makeSchemaInternal({ outputs: { typegen: path.join(__dirname, "test-gen.ts"), schema: path.join(__dirname, "test-gen.graphql"), @@ -33,25 +35,30 @@ describe("typegen", () => { ], contextType: "t.TestContext", }, + types: [ + schema.getTypeMap(), + scalarType({ + name: "CustomScalarMethod", + parseLiteral() {}, + parseValue() {}, + serialize() {}, + asNexusMethod: "CustomScalarMethod", + }), + decorateType( + new GraphQLScalarType({ + name: "DecoratedCustomScalar", + parseLiteral() {}, + parseValue() {}, + serialize() {}, + }), + { + asNexusMethod: "DecoratedCustomScalar", + } + ), + ], }); - const typegenInfo = await metadata.getTypegenInfo(schema); - typegen = new Typegen( - schema, - { - ...typegenInfo, - typegenFile: "", - }, - { - rootTypings: {}, - dynamicFields: { - dynamicInputFields: {}, - dynamicOutputFields: {}, - dynamicOutputProperties: {}, - }, - } - ); jest - .spyOn(typegen, "hasResolver") + .spyOn(TypegenPrinter.prototype, "hasResolver") .mockImplementation( ( field: GraphQLField, @@ -63,6 +70,8 @@ describe("typegen", () => { return false; } ); + metadata = new TypegenMetadata(builder.builder, builder.schema); + typegen = new TypegenPrinter(metadata, await metadata.getTypegenInfo()); }); it("builds the enum object type defs", () => { @@ -87,7 +96,8 @@ describe("typegen", () => { // If the field has a resolver, we assume it's derived, otherwise // you'll need to supply a backing root type with more information. jest - .spyOn(typegen, "hasResolver") + .clearAllMocks() + .spyOn(TypegenPrinter.prototype, "hasResolver") .mockImplementation( ( field: GraphQLField, diff --git a/tests/unionType.spec.ts b/tests/unionType.spec.ts new file mode 100644 index 00000000..545dc8fd --- /dev/null +++ b/tests/unionType.spec.ts @@ -0,0 +1,96 @@ +import { graphql } from "graphql"; +import path from "path"; +import { + deferred, + makeSchema, + objectType, + queryField, + unionType, +} from "../src/core"; +import { FileSystem } from "../src/fileSystem"; + +describe("unionType", () => { + let spy: jest.SpyInstance; + beforeEach(() => { + jest.clearAllMocks(); + spy = jest + .spyOn(FileSystem.prototype, "replaceFile") + .mockImplementation(async () => null); + }); + + test("unionType", async () => { + const dfd = deferred(); + const schema = makeSchema({ + types: [ + objectType({ + name: "DeletedUser", + definition(t) { + t.string("message", (root) => `This user ${root.id} was deleted`); + }, + rootTyping: `{ id: number; deletedAt: Date }`, + }), + objectType({ + name: "User", + definition(t) { + t.int("id"); + t.string("name"); + }, + rootTyping: `{ id: number; name: string; deletedAt?: null }`, + }), + unionType({ + name: "UserOrError", + definition(t) { + t.members("User", "DeletedUser"); + t.resolveType((o) => (o.deletedAt ? "DeletedUser" : "User")); + }, + }), + queryField("userTest", { + type: "UserOrError", + resolve: () => ({ id: 1, name: "Test User" }), + }), + queryField("deletedUserTest", { + type: "UserOrError", + resolve: () => ({ + id: 1, + name: "Test User", + deletedAt: new Date("2019-01-01"), + }), + }), + ], + onReady: dfd.resolve, + outputs: { + schema: path.join(__dirname, "unionTypeTest.graphql"), + typegen: false, + }, + shouldGenerateArtifacts: true, + }); + await dfd.promise; + expect(spy).toBeCalledTimes(1); + expect(spy.mock.calls[0]).toMatchSnapshot(); + expect( + await graphql( + schema, + ` + fragment UserOrErrorFields on UserOrError { + __typename + ... on User { + id + name + } + ... on DeletedUser { + message + } + } + query UserOrErrorTest { + userTest { + ...UserOrErrorFields + } + deletedUserTest { + ...UserOrErrorFields + } + } + ` + ) + ).toMatchSnapshot(); + }); +}); diff --git a/tests/unknownType.spec.ts b/tests/unknownType.spec.ts index 2c0c0657..9811a530 100644 --- a/tests/unknownType.spec.ts +++ b/tests/unknownType.spec.ts @@ -1,6 +1,18 @@ -import { objectType, makeSchemaInternal, makeSchema } from "../src/core"; +import * as path from "path"; +import { + objectType, + makeSchemaInternal, + makeSchema, + UNKNOWN_TYPE_SCALAR, +} from "../src/core"; +import { FileSystem } from "../src/fileSystem"; +import { Kind } from "graphql"; describe("unknownType", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + const Query = objectType({ name: "Query", definition(t) { @@ -29,6 +41,22 @@ describe("unknownType", () => { }, }); + test("UNKNOWN_TYPE_SCALAR is a scalar, with identity for the implementation", () => { + const obj = {}; + expect(() => { + UNKNOWN_TYPE_SCALAR.parseLiteral( + { value: "123.45", kind: Kind.FLOAT }, + {} + ); + }).toThrowError("Error: NEXUS__UNKNOWN__TYPE is not a valid scalar."); + expect(() => UNKNOWN_TYPE_SCALAR.parseValue(obj)).toThrowError( + "Error: NEXUS__UNKNOWN__TYPE is not a valid scalar." + ); + expect(() => UNKNOWN_TYPE_SCALAR.serialize(obj)).toThrowError( + "Error: NEXUS__UNKNOWN__TYPE is not a valid scalar." + ); + }); + test("schema should build without throwing", () => { expect(() => { makeSchemaInternal({ @@ -48,7 +76,7 @@ describe("unknownType", () => { expect(Object.keys(missingTypes)).toContain("User"); }); - test("should render the typegen but throw", () => { + test("should throw immediately if not generating artifacts", () => { try { makeSchema({ types: [Query, User], @@ -56,13 +84,38 @@ describe("unknownType", () => { schema: false, typegen: false, }, - shouldGenerateArtifacts: true, + shouldGenerateArtifacts: false, }); } catch (e) { - expect(e).toMatchInlineSnapshot(` -[Error: -- Missing type User, did you forget to import a type to the root query?] + expect(e.message).toMatchInlineSnapshot(` +" +- Missing type User, did you forget to import a type to the root query?" `); } }); + + test("should render the typegen but throw in the next-tick", (done) => { + const spy = jest + .spyOn(FileSystem.prototype, "replaceFile") + .mockImplementation(async () => null); + process.setUncaughtExceptionCaptureCallback((e) => { + expect(spy).toBeCalledTimes(2); + expect(spy.mock.calls[0]).toMatchSnapshot(); + expect(spy.mock.calls[1]).toMatchSnapshot(); + expect(e.message).toMatchInlineSnapshot(` +" +- Missing type User, did you forget to import a type to the root query?" +`); + done(); + }); + + makeSchema({ + types: [Query, User], + outputs: { + schema: path.join(__dirname, "test.graphql"), + typegen: path.join(__dirname, "test.ts"), + }, + shouldGenerateArtifacts: true, + }); + }); }); diff --git a/website/playground/monaco-config.ts b/website/playground/monaco-config.ts index bf8d716c..e24e5313 100644 --- a/website/playground/monaco-config.ts +++ b/website/playground/monaco-config.ts @@ -22,7 +22,6 @@ const allTypeDefs = [ require("raw-loader!nexus/dist/utils.d.ts"), require("raw-loader!nexus/dist/typegenTypeHelpers.d.ts"), require("raw-loader!nexus/dist/typegenMetadata.d.ts"), - require("raw-loader!nexus/dist/typegenFormatPrettier.d.ts"), require("raw-loader!nexus/dist/typegenAutoConfig.d.ts"), require("raw-loader!nexus/dist/typegen.d.ts"), require("raw-loader!nexus/dist/sdlConverter.d.ts"), @@ -57,7 +56,6 @@ const files = [ "nexus/utils.d.ts", "nexus/typegenTypeHelpers.d.ts", "nexus/typegenMetadata.d.ts", - "nexus/typegenFormatPrettier.d.ts", "nexus/typegenAutoConfig.d.ts", "nexus/typegen.d.ts", "nexus/sdlConverter.d.ts", diff --git a/yarn.lock b/yarn.lock index d2520320..454470df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,10 +25,17 @@ dependencies: any-observable "^0.3.0" -"@types/graphql@14.0.7": - version "14.0.7" - resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.0.7.tgz#daa09397220a68ce1cbb3f76a315ff3cd92312f6" - integrity sha512-BoLDjdvLQsXPZLJux3lEZANwGr3Xag56Ngy0U3y8uoRSDdeLcn43H3oBcgZlnd++iOQElBpaRVDHPzEDekyvXQ== +"@types/graphql-iso-date@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/graphql-iso-date/-/graphql-iso-date-3.3.1.tgz#dbb540ae62c68c00eba1d1feb4602d7209257e9d" + integrity sha512-x64IejUTqiiC2NGMgMYVOsKgViKknfnhzJHS8pMiYdDqbR5Fd9XHAkujGYvAOBkjFB6TDunY6S8uLDT/OnrKBA== + dependencies: + "@types/graphql" "*" + +"@types/graphql@*", "@types/graphql@^14.2.2": + version "14.2.2" + resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.2.2.tgz#10f197e6f8559c11b16d630c5e9c10c3c8e61c5e" + integrity sha512-okXbUmdZFMO3AYBEJCcpJFPFDkKmIiZZBqWD5TmPtAv+GHfjD2qLZEI0PvZ8IWMU4ozoK2HV2lDxWjw4LbVlnw== "@types/jest@^23.3.7": version "23.3.10" @@ -1358,10 +1365,15 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== -graphql@^14.0.2: - version "14.0.2" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.0.2.tgz#7dded337a4c3fd2d075692323384034b357f5650" - integrity sha512-gUC4YYsaiSJT1h40krG3J+USGlwhzNTXSb4IOZljn9ag5Tj+RkoXrWp+Kh7WyE3t1NCfab5kzCuxBIvOMERMXw== +graphql-iso-date@3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/graphql-iso-date/-/graphql-iso-date-3.6.1.tgz#bd2d0dc886e0f954cbbbc496bbf1d480b57ffa96" + integrity sha512-AwFGIuYMJQXOEAgRlJlFL4H1ncFM8n8XmoVDTNypNOZyQ8LFDG2ppMFlsS862BSTCDcSUfHp8PD3/uJhv7t59Q== + +graphql@^14.2.0: + version "14.4.2" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.4.2.tgz#553a7d546d524663eda49ed6df77577be3203ae3" + integrity sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ== dependencies: iterall "^1.2.2" @@ -3627,6 +3639,14 @@ source-map-support@^0.4.15: dependencies: source-map "^0.5.6" +source-map-support@^0.5.12: + version "0.5.12" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" + integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-support@^0.5.6: version "0.5.9" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f"