Improving code coverage (#225)

* Improving overall coverage
* Improve jest config
* Remove unused internal builder methods
* Prefer non-inline snapshots
This commit is contained in:
Tim Griesser 2019-09-22 21:28:27 -04:00 committed by GitHub
parent e58664e671
commit 9f4ab9d830
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1581 additions and 560 deletions

View File

@ -10,6 +10,8 @@ jobs:
- run: yarn build
- run: yarn lint
- run: yarn test:ci
- store_artifacts:
path: coverage
example_apollo-fullstack:
docker:

View File

@ -1,3 +1,5 @@
const path = require("path");
/**
* @type {jest.InitialOptions}
*/
@ -11,12 +13,8 @@ module.exports = {
],
globals: {
"ts-jest": {
isolatedModules: !process.env.CI,
diagnostics: {
// FIXME: sloppy, can we make { core } not output in
// typegen when its not needed?
ignoreCodes: ["6133"],
},
tsConfig: path.join(__dirname, "tests/tsconfig.json"),
},
},
collectCoverageFrom: ["src/**/*"],
};

View File

@ -9,7 +9,7 @@
"scripts": {
"dev": "yarn link-examples && tsc -w",
"test": "jest",
"test:ci": "yarn add file:. && jest -i",
"test:ci": "yarn add file:. && jest -i --coverage",
"build": "tsc",
"lint": "tslint -p tsconfig.json",
"clean": "rm -rf dist",
@ -43,9 +43,11 @@
"@types/jest": "^23.3.7",
"@types/node": "^10.12.2",
"@types/prettier": "^1.15.2",
"@types/graphql-iso-date": "^3.3.3",
"graphql": "^14.0.2",
"husky": "^1.1.2",
"jest": "^24",
"graphql-iso-date": "^3.6.1",
"jest-watch-typeahead": "^0.3.1",
"lint-staged": "^7.3.0",
"nexus": "file:.",

View File

@ -23,14 +23,12 @@ import {
GraphQLSchema,
GraphQLString,
GraphQLUnionType,
isEnumType,
isInputObjectType,
isInterfaceType,
isLeafType,
isNamedType,
isObjectType,
isOutputType,
isUnionType,
isScalarType,
defaultFieldResolver,
assertValidName,
@ -127,6 +125,7 @@ import {
} from "./definitions/extendInputType";
import { DynamicInputMethodDef, DynamicOutputMethodDef } from "./dynamicMethod";
import { DynamicOutputPropertyDef } from "./dynamicProperty";
import { decorateType } from "./definitions/decorateType";
export type Maybe<T> = T | null;
@ -148,18 +147,28 @@ const SCALARS: Record<string, GraphQLScalarType> = {
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 {
/**
@ -535,6 +544,7 @@ export class SchemaBuilder {
addField: (field) => fields.push(field),
addDynamicInputFields: (block, isList) =>
this.addDynamicInputFields(block, isList),
warn: consoleWarn,
});
config.definition(definitionBlock);
const extensions = this.inputTypeExtensionMap[config.name];
@ -576,6 +586,7 @@ export class SchemaBuilder {
},
addDynamicOutputMembers: (block, isList) =>
this.addDynamicOutputMembers(block, isList),
warn: consoleWarn,
});
config.definition(definitionBlock);
const extensions = this.typeExtensionMap[config.name];
@ -645,6 +656,7 @@ export class SchemaBuilder {
setResolveType: (fn) => (resolveType = fn),
addDynamicOutputMembers: (block, isList) =>
this.addDynamicOutputMembers(block, isList),
warn: consoleWarn,
});
config.definition(definitionBlock);
const extensions = this.typeExtensionMap[config.name];
@ -974,36 +986,6 @@ export class SchemaBuilder {
return type;
}
protected getEnum(name: string): GraphQLEnumType {
const type = this.getOrBuildType(name);
if (!isEnumType(type)) {
throw new Error(
`Expected ${name} to be an enumType, saw ${type.constructor.name}`
);
}
return type;
}
protected getUnion(name: string): GraphQLUnionType {
const type = this.getOrBuildType(name);
if (!isUnionType(type)) {
throw new Error(
`Expected ${name} to be a unionType, saw ${type.constructor.name}`
);
}
return type;
}
protected getInputObjectType(name: string): GraphQLInputObjectType {
const type = this.getOrBuildType(name);
if (!isInputObjectType(type)) {
throw new Error(
`Expected ${name} to be a valid input type, saw ${type.constructor.name}`
);
}
return type;
}
protected getInputType(
name:
| string
@ -1125,9 +1107,9 @@ export class SchemaBuilder {
missingResolveType(name: string, location: "union" | "interface") {
console.error(
new Error(
`Missing resolveType for the ${name} ${location}.` +
`Missing resolveType for the ${name} ${location}. ` +
`Be sure to add one in the definition block for the type, ` +
`or t.resolveType(() => null) if you don't want to implement yet`
`or t.resolveType(() => null) if you don't want or need to implement.`
)
);
return () => null;
@ -1139,6 +1121,7 @@ export class SchemaBuilder {
addField: (f) => this.maybeTraverseInputType(f),
addDynamicInputFields: (block, isList) =>
this.addDynamicInputFields(block, isList),
warn: () => {},
});
obj.definition(definitionBlock);
return obj;
@ -1223,6 +1206,7 @@ export class SchemaBuilder {
addField: (f) => this.maybeTraverseOutputType(f),
addDynamicOutputMembers: (block, isList) =>
this.addDynamicOutputMembers(block, isList),
warn: () => {},
});
obj.definition(definitionBlock);
return obj;
@ -1235,6 +1219,7 @@ export class SchemaBuilder {
addField: (f) => this.maybeTraverseOutputType(f),
addDynamicOutputMembers: (block, isList) =>
this.addDynamicOutputMembers(block, isList),
warn: () => {},
});
obj.definition(definitionBlock);
return obj;
@ -1531,3 +1516,7 @@ function assertNoMissingTypes(
throw new Error("\n" + errors);
}
}
function consoleWarn(msg: string) {
console.warn(msg);
}

View File

@ -1,8 +1,9 @@
import { GraphQLNamedType } from "graphql";
import { RootTypingDef } from "./_types";
import { Maybe } from "../core";
export interface TypeExtensionConfig {
asNexusMethod: string;
asNexusMethod?: string;
rootTyping?: RootTypingDef;
}
@ -11,7 +12,7 @@ export type NexusTypeExtensions = {
};
export function decorateType<T extends GraphQLNamedType>(
type: T & { extensions?: NexusTypeExtensions },
type: T & { extensions?: Maybe<Readonly<Record<string, any>>> },
config: TypeExtensionConfig
): T & { extensions: NexusTypeExtensions } {
type.extensions = {

View File

@ -132,6 +132,7 @@ export interface OutputDefinitionBuilder {
block: OutputDefinitionBlock<any>,
isList: boolean
): void;
warn(msg: string): void;
}
export interface InputDefinitionBuilder {
@ -141,6 +142,7 @@ export interface InputDefinitionBuilder {
block: InputDefinitionBlock<any>,
isList: boolean
): void;
warn(msg: string): void;
}
export interface OutputDefinitionBlock<TypeName extends string>
@ -242,7 +244,7 @@ export class OutputDefinitionBlock<TypeName extends string> {
protected decorateField(config: NexusOutputFieldDef): NexusOutputFieldDef {
if (this.isList) {
if (config.list) {
console.warn(
this.typeBuilder.warn(
`It looks like you chained .list and set list for ${config.name}. ` +
"You should only do one or the other"
);
@ -348,7 +350,7 @@ export class InputDefinitionBlock<TypeName extends string> {
protected decorateField(config: NexusInputFieldDef): NexusInputFieldDef {
if (this.isList) {
if (config.list) {
console.warn(
this.typeBuilder.warn(
`It looks like you chained .list and set list for ${config.name}` +
"You should only do one or the other"
);

View File

@ -5,8 +5,8 @@ import { intArg } from "../definitions/args";
const basicCollectionMap = new Map<string, NexusObjectTypeDef<string>>();
export const Collection = dynamicOutputMethod({
name: "collection",
export const CollectionFieldMethod = dynamicOutputMethod({
name: "collectionField",
typeDefinition: `<FieldName extends string>(fieldName: FieldName, opts: {
type: NexusGenObjectNames | NexusGenInterfaceNames | core.NexusObjectTypeDef<string> | core.NexusInterfaceTypeDef<string>,
nodes: core.SubFieldResolver<TypeName, FieldName, "nodes">,
@ -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(),

View File

@ -7,18 +7,25 @@ const relayConnectionMap = new Map<string, NexusObjectTypeDef<string>>();
let pageInfo: NexusObjectTypeDef<string>;
export const RelayConnection = dynamicOutputMethod({
name: "relayConnection",
typeDefinition: `<FieldName extends string>(fieldName: FieldName, opts: {
export const RelayConnectionFieldMethod = dynamicOutputMethod({
name: "relayConnectionField",
typeDefinition: `
<FieldName extends string>(fieldName: FieldName, opts: {
type: NexusGenObjectNames | NexusGenInterfaceNames | core.NexusObjectTypeDef<string> | core.NexusInterfaceTypeDef<string>,
edges: core.SubFieldResolver<TypeName, FieldName, "edges">,
pageInfo: core.SubFieldResolver<TypeName, FieldName, "pageInfo">,
args?: Record<string, core.NexusArgDef<string>>,
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(),

View File

@ -23,10 +23,6 @@ export function log(msg: string) {
console.log(`GraphQL Nexus: ${msg}`);
}
export function withDeprecationComment(description?: string | null) {
return description;
}
export const isInterfaceField = (
type: GraphQLObjectType,
fieldName: string
@ -217,6 +213,7 @@ export function firstDefined<T>(...args: Array<T | undefined>): T {
return arg;
}
}
/* istanbul ignore next */
throw new Error("At least one of the values should be defined");
}

View File

@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`interfaceType can be implemented by object types 1`] = `
Object {
"data": Object {
"user": Object {
"id": "User:1",
"name": "Test User",
},
},
}
`;
exports[`interfaceType throws if the arg is not provided to the type 1`] = `"You must provide a \\"type\\" for the arg()"`;

View File

@ -1,5 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`backingTypes can match backing types for const enums 1`] = `
"export interface NexusGenEnums {
B: t.B
}"
`;
exports[`backingTypes can match backing types to regular enums 1`] = `
"export interface NexusGenEnums {
A: t.A
}"
`;
exports[`rootTypings can import enum via rootTyping 1`] = `
"/**
* This file was automatically generated by GraphQL Nexus

View File

@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`extendInputType should allow extending input objects 1`] = `
"input InputTest {
hello: String
world: String
}"
`;
exports[`extendType should allow adding types to the Query type 1`] = `
Array [
"user",
"post",
]
`;
exports[`inputObjectType should output lists properly, #33 1`] = `
"input AddToBasketInput {
extras: [ExtraBasketInput!]
}"
`;

View File

@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`dynamicOutputMethod CollectionFieldMethod example 1`] = `
Object {
"data": Object {
"cats": Object {
"nodes": Array [
Object {
"id": "Cat:1",
"name": "Felix",
},
Object {
"id": "Cat:2",
"name": "Booker",
},
],
"totalCount": 2,
},
},
}
`;
exports[`dynamicOutputMethod RelayConnectionFieldMethod example 1`] = `
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,
},
},
},
}
`;

View File

@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`inputObject builds creates an inputObject type 1`] = `
Object {
"errors": Array [
[GraphQLError: Unknown argument "input" on field "user" of type "Query".],
],
}
`;
exports[`inputObject has asArg for using one-off inputObjects inline 1`] = `
Object {
"errors": Array [
[GraphQLError: Unknown argument "input" on field "user" of type "Query".],
],
}
`;
exports[`inputObject throws when chaining .list twice 1`] = `"Cannot chain list.list, in the definition block. Use \`list: []\` config value"`;
exports[`inputObject warns when specifying .list and list: true 1`] = `
Array [
"It looks like you chained .list and set list for someFieldYou should only do one or the other",
]
`;

View File

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`interfaceType can be implemented by object types 1`] = `
Object {
"data": Object {
"user": Object {
"id": "User:1",
"name": "Test User",
},
},
}
`;
exports[`interfaceType logs error when resolveType is not provided for an interface 1`] = `
Array [
[Error: Missing resolveType for the Node union. Be sure to add one in the definition block for the type, or t.resolveType(() => null) if you don't want or need to implement.],
]
`;

View File

@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`nonNullDefaults false/false on schema 1`] = `
"type Query {
test(test: Int): Boolean
}
"
`;
exports[`nonNullDefaults false/false on type 1`] = `
"type Query {
test(test: Int): Boolean
}
"
`;
exports[`nonNullDefaults true/true on schema 1`] = `
"type Query {
test(test: Int!): Boolean!
}
"
`;
exports[`nonNullDefaults true/true on type 1`] = `
"type Query {
test(test: Int!): Boolean!
}
"
`;

View File

@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`objectType builds creates an object type 1`] = `
Object {
"data": Object {
"user": Object {
"floatField": 123.4,
"id": "User:1",
"name": "Test User",
},
},
}
`;
exports[`objectType throws when chaining .list twice 1`] = `"Cannot chain list.list, in the definition block. Use \`list: []\` config value"`;
exports[`objectType warns when specifying .list and list: true 1`] = `
Array [
"It looks like you chained .list and set list for someField. You should only do one or the other",
]
`;

View File

@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`scalarType asNexusMethod: should wrap a scalar and make it available on the builder 1`] = `
Object {
"data": Object {
"user": Object {
"dateTimeField": "2020-01-01T00:00:00.000Z",
"id": "User:1",
},
},
}
`;

View File

@ -0,0 +1,396 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SDLConverter printEnumTypes 1`] = `
"export const OrderEnum = enumType({
name: \\"OrderEnum\\",
members: [\\"ASC\\",\\"DESC\\"],
});
export const SomeEnum = enumType({
name: \\"SomeEnum\\",
members: [\\"A\\",{\\"name\\":\\"B\\",\\"deprecation\\":\\"This is a deprecation reason for B\\",\\"value\\":\\"B\\"}],
});"
`;
exports[`SDLConverter printObjectTypes 1`] = `
"export const Mutation = objectType({
name: \\"Mutation\\",
definition(t) {
t.string(\\"someList\\", {
list: [false],
args: {
items: stringArg({
list: [false],
required: true
}),
},
})
t.field(\\"createPost\\", {
type: Post,
args: {
input: arg({
type: CreatePostInput,
required: true
}),
},
})
t.field(\\"registerClick\\", {
type: Query,
args: {
uuid: uuidArg(),
},
})
}
})
export const Post = objectType({
name: \\"Post\\",
description: \\"This is a description of a Post\\",
definition(t) {
t.implements(Node)
t.uuid(\\"uuid\\")
t.field(\\"author\\", { type: User })
t.float(\\"geo\\", { list: [true, true] })
t.float(\\"messyGeo\\", {
list: [true, false],
nullable: true,
})
}
})
export const Query = objectType({
name: \\"Query\\",
definition(t) {
t.field(\\"user\\", { type: User })
t.list.field(\\"posts\\", {
type: Post,
args: {
filters: arg({
type: PostFilters,
required: true
}),
},
})
t.field(\\"unionField\\", { type: ExampleUnion })
}
})
export const User = objectType({
name: \\"User\\",
definition(t) {
t.implements(Node)
t.string(\\"name\\", {
description: \\"This is a description of a name\\",
args: {
prefix: stringArg({ description: \\"And a description of an arg\\" }),
},
})
t.string(\\"email\\")
t.string(\\"phone\\", { nullable: true })
t.list.field(\\"posts\\", {
type: Post,
args: {
filters: arg({ type: PostFilters }),
},
})
t.field(\\"outEnum\\", {
type: SomeEnum,
nullable: true,
})
}
})"
`;
exports[`convertSDL 1`] = `
"import { objectType, stringArg, arg, uuidArg, interfaceType, inputObjectType, unionType, enumType, scalarType } from 'nexus';
export const Mutation = objectType({
name: \\"Mutation\\",
definition(t) {
t.string(\\"someList\\", {
list: [false],
args: {
items: stringArg({
list: [false],
required: true
}),
},
})
t.field(\\"createPost\\", {
type: Post,
args: {
input: arg({
type: CreatePostInput,
required: true
}),
},
})
t.field(\\"registerClick\\", {
type: Query,
args: {
uuid: uuidArg(),
},
})
}
})
export const Post = objectType({
name: \\"Post\\",
description: \\"This is a description of a Post\\",
definition(t) {
t.implements(Node)
t.uuid(\\"uuid\\")
t.field(\\"author\\", { type: User })
t.float(\\"geo\\", { list: [true, true] })
t.float(\\"messyGeo\\", {
list: [true, false],
nullable: true,
})
}
})
export const Query = objectType({
name: \\"Query\\",
definition(t) {
t.field(\\"user\\", { type: User })
t.list.field(\\"posts\\", {
type: Post,
args: {
filters: arg({
type: PostFilters,
required: true
}),
},
})
t.field(\\"unionField\\", { type: ExampleUnion })
}
})
export const User = objectType({
name: \\"User\\",
definition(t) {
t.implements(Node)
t.string(\\"name\\", {
description: \\"This is a description of a name\\",
args: {
prefix: stringArg({ description: \\"And a description of an arg\\" }),
},
})
t.string(\\"email\\")
t.string(\\"phone\\", { nullable: true })
t.list.field(\\"posts\\", {
type: Post,
args: {
filters: arg({ type: PostFilters }),
},
})
t.field(\\"outEnum\\", {
type: SomeEnum,
nullable: true,
})
}
})
export const Node = interfaceType({
name: \\"Node\\",
description: \\"This is a description of a Node\\",
definition(t) {
t.id(\\"id\\")
t.resolveType(() => null)
}
});
export const CreatePostInput = inputObjectType({
name: \\"CreatePostInput\\",
definition(t) {
t.string(\\"name\\", { required: true })
t.id(\\"author\\", { required: true })
t.float(\\"geo\\", {
list: [false, true],
required: true,
})
}
});
export const PostFilters = inputObjectType({
name: \\"PostFilters\\",
definition(t) {
t.field(\\"order\\", {
type: OrderEnum,
required: true,
})
t.string(\\"search\\")
}
});
export const ExampleUnion = unionType({
name: \\"ExampleUnion\\",
definition(t) {
t.members(Post, User)
}
});
export const OrderEnum = enumType({
name: \\"OrderEnum\\",
members: [\\"ASC\\",\\"DESC\\"],
});
export const SomeEnum = enumType({
name: \\"SomeEnum\\",
members: [\\"A\\",{\\"name\\":\\"B\\",\\"deprecation\\":\\"This is a deprecation reason for B\\",\\"value\\":\\"B\\"}],
});
export const UUID = scalarType({
name: \\"UUID\\",
asNexusMethod: \\"uuid\\",
serialize() { /* Todo */ },
parseValue() { /* Todo */ },
parseLiteral() { /* Todo */ }
});"
`;
exports[`convertSDL as commonjs 1`] = `
"const { objectType, stringArg, arg, uuidArg, interfaceType, inputObjectType, unionType, enumType, scalarType } = require('nexus');
const Mutation = objectType({
name: \\"Mutation\\",
definition(t) {
t.string(\\"someList\\", {
list: [false],
args: {
items: stringArg({
list: [false],
required: true
}),
},
})
t.field(\\"createPost\\", {
type: Post,
args: {
input: arg({
type: CreatePostInput,
required: true
}),
},
})
t.field(\\"registerClick\\", {
type: Query,
args: {
uuid: uuidArg(),
},
})
}
})
const Post = objectType({
name: \\"Post\\",
description: \\"This is a description of a Post\\",
definition(t) {
t.implements(Node)
t.uuid(\\"uuid\\")
t.field(\\"author\\", { type: User })
t.float(\\"geo\\", { list: [true, true] })
t.float(\\"messyGeo\\", {
list: [true, false],
nullable: true,
})
}
})
const Query = objectType({
name: \\"Query\\",
definition(t) {
t.field(\\"user\\", { type: User })
t.list.field(\\"posts\\", {
type: Post,
args: {
filters: arg({
type: PostFilters,
required: true
}),
},
})
t.field(\\"unionField\\", { type: ExampleUnion })
}
})
const User = objectType({
name: \\"User\\",
definition(t) {
t.implements(Node)
t.string(\\"name\\", {
description: \\"This is a description of a name\\",
args: {
prefix: stringArg({ description: \\"And a description of an arg\\" }),
},
})
t.string(\\"email\\")
t.string(\\"phone\\", { nullable: true })
t.list.field(\\"posts\\", {
type: Post,
args: {
filters: arg({ type: PostFilters }),
},
})
t.field(\\"outEnum\\", {
type: SomeEnum,
nullable: true,
})
}
})
const Node = interfaceType({
name: \\"Node\\",
description: \\"This is a description of a Node\\",
definition(t) {
t.id(\\"id\\")
t.resolveType(() => null)
}
});
const CreatePostInput = inputObjectType({
name: \\"CreatePostInput\\",
definition(t) {
t.string(\\"name\\", { required: true })
t.id(\\"author\\", { required: true })
t.float(\\"geo\\", {
list: [false, true],
required: true,
})
}
});
const PostFilters = inputObjectType({
name: \\"PostFilters\\",
definition(t) {
t.field(\\"order\\", {
type: OrderEnum,
required: true,
})
t.string(\\"search\\")
}
});
const ExampleUnion = unionType({
name: \\"ExampleUnion\\",
definition(t) {
t.members(Post, User)
}
});
const OrderEnum = enumType({
name: \\"OrderEnum\\",
members: [\\"ASC\\",\\"DESC\\"],
});
const SomeEnum = enumType({
name: \\"SomeEnum\\",
members: [\\"A\\",{\\"name\\":\\"B\\",\\"deprecation\\":\\"This is a deprecation reason for B\\",\\"value\\":\\"B\\"}],
});
const UUID = scalarType({
name: \\"UUID\\",
asNexusMethod: \\"uuid\\",
serialize() { /* Todo */ },
parseValue() { /* Todo */ },
parseLiteral() { /* Todo */ }
});
exports.Mutation = Mutation;
exports.Post = Post;
exports.Query = Query;
exports.User = User;
exports.Node = Node;
exports.CreatePostInput = CreatePostInput;
exports.PostFilters = PostFilters;
exports.ExampleUnion = ExampleUnion;
exports.OrderEnum = OrderEnum;
exports.SomeEnum = SomeEnum;
exports.UUID = UUID;"
`;

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`unionType unionType 1`] = `
Object {
"data": Object {
"deletedUserTest": Object {
"__typename": "DeletedUser",
"message": "This user 1 was deleted",
},
"userTest": Object {
"__typename": "User",
"id": 1,
"name": "Test User",
},
},
}
`;

View File

@ -0,0 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`unknownType should render the typegen but throw 1`] = `
[Error:
- Missing type User, did you forget to import a type to the root query?]
`;

4
tests/_fixtures.ts Normal file
View File

@ -0,0 +1,4 @@
export const CatListFixture = [
{ id: "Cat:1", name: "Felix" },
{ id: "Cat:2", name: "Booker" },
];

61
tests/args.spec.ts Normal file
View File

@ -0,0 +1,61 @@
import { graphql } from "graphql";
import {
makeSchema,
queryField,
booleanArg,
floatArg,
idArg,
stringArg,
intArg,
objectType,
arg,
} from "../src/core";
describe("interfaceType", () => {
let schema: ReturnType<typeof makeSchema>;
beforeAll(() => {
schema = makeSchema({
types: [
queryField("user", {
type: "User",
args: {
int: intArg(),
bool: booleanArg(),
float: floatArg(),
id: idArg(),
str: stringArg(),
},
resolve: () => ({ id: `User:1`, name: "Test User" }),
}),
objectType({
name: "User",
definition(t) {
t.id("id");
t.string("name");
},
}),
],
outputs: false,
shouldGenerateArtifacts: false,
});
});
it("can be implemented by object types", async () => {
expect(
await graphql(
schema,
`
{
user(int: 1, bool: true, float: 123.45, str: "Test") {
id
name
}
}
`
)
).toMatchSnapshot();
});
it("throws if the arg is not provided to the type", async () => {
// @ts-ignore
expect(() => arg({ type: null })).toThrowErrorMatchingSnapshot();
});
});

View File

@ -73,11 +73,7 @@ describe("backingTypes", () => {
(schema as any).extensions.nexus
);
expect(typegen.printEnumTypeMap()).toMatchInlineSnapshot(`
"export interface NexusGenEnums {
A: t.A
}"
`);
expect(typegen.printEnumTypeMap()).toMatchSnapshot();
});
it("can match backing types for const enums", async () => {
@ -89,11 +85,7 @@ describe("backingTypes", () => {
(schema as any).extensions.nexus
);
expect(typegen.printEnumTypeMap()).toMatchInlineSnapshot(`
"export interface NexusGenEnums {
B: t.B
}"
`);
expect(typegen.printEnumTypeMap()).toMatchSnapshot();
});
});

View File

@ -189,12 +189,7 @@ describe("extendType", () => {
UserObject,
]).typeMap.Query.getFields()
)
).toMatchInlineSnapshot(`
Array [
"user",
"post",
]
`);
).toMatchSnapshot();
});
});
@ -214,12 +209,7 @@ describe("inputObjectType", () => {
},
}),
]);
expect(printType(buildTypesMap.typeMap.AddToBasketInput))
.toMatchInlineSnapshot(`
"input AddToBasketInput {
extras: [ExtraBasketInput!]
}"
`);
expect(printType(buildTypesMap.typeMap.AddToBasketInput)).toMatchSnapshot();
});
});
@ -239,11 +229,6 @@ describe("extendInputType", () => {
},
}),
]);
expect(printType(buildTypesMap.typeMap.InputTest)).toMatchInlineSnapshot(`
"input InputTest {
hello: String
world: String
}"
`);
expect(printType(buildTypesMap.typeMap.InputTest)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,230 @@
import path from "path";
import { GraphQLDateTime } from "graphql-iso-date";
import {
makeSchema,
objectType,
queryType,
inputObjectType,
dynamicInputMethod,
decorateType,
} from "../src/core";
import {
RelayConnectionFieldMethod,
CollectionFieldMethod,
} from "../src/extensions";
import { graphql } from "graphql";
import { CatListFixture } from "./_fixtures";
import { dynamicOutputProperty } from "../src/dynamicProperty";
let spy: jest.SpyInstance;
beforeEach(() => {
jest.clearAllMocks();
});
describe("dynamicOutputMethod", () => {
const Cat = objectType({
name: "Cat",
definition(t) {
t.id("id");
t.string("name");
},
});
test("RelayConnectionFieldMethod example", async () => {
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, RelayConnectionFieldMethod],
outputs: {
typegen: path.join(__dirname, "test-output.ts"),
schema: path.join(__dirname, "schema.graphql"),
},
shouldGenerateArtifacts: false,
});
expect(
await graphql(
schema,
`
{
cats {
edges {
node {
id
name
}
}
pageInfo {
hasNextPage
hasPreviousPage
}
}
}
`
)
).toMatchSnapshot();
});
test("CollectionFieldMethod example", async () => {
const dynamicOutputMethod = queryType({
definition(t) {
// @ts-ignore
t.collectionField("cats", {
type: Cat,
totalCount: () => CatListFixture.length,
nodes: () => CatListFixture,
});
},
});
const schema = makeSchema({
types: [dynamicOutputMethod, CollectionFieldMethod],
outputs: {
typegen: path.join(__dirname, "test-output"),
schema: path.join(__dirname, "schema.graphql"),
},
shouldGenerateArtifacts: false,
});
expect(
await graphql(
schema,
`
{
cats {
totalCount
nodes {
id
name
}
}
}
`
)
).toMatchSnapshot();
});
test("CollectionFieldMethod example with string type ref", () => {
makeSchema({
types: [
queryType({
definition(t) {
// @ts-ignore
t.collectionField("cats", {
type: "Cat",
totalCount: () => CatListFixture.length,
nodes: () => CatListFixture,
});
},
}),
CollectionFieldMethod,
],
outputs: false,
});
});
test("RelayConnectionFieldMethod 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,
})),
});
},
}),
RelayConnectionFieldMethod,
],
outputs: false,
});
});
});
describe("dynamicInputMethod", () => {
it("should provide a method on the input definition", async () => {
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"),
},
shouldGenerateArtifacts: false,
});
});
});
describe("dynamicOutputProperty", () => {
it("should provide a way for adding a chainable api on the output definition", async () => {
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"),
},
shouldGenerateArtifacts: false,
});
});
});

View File

@ -0,0 +1,139 @@
import { graphql } from "graphql";
import {
makeSchema,
objectType,
queryField,
inputObjectType,
} from "../src/core";
describe("inputObject", () => {
it("builds creates an inputObject type", async () => {
const schema = makeSchema({
types: [
inputObjectType({
name: "InputObj",
definition(t) {
t.id("idInput");
t.boolean("boolInput");
t.float("floatInput");
t.int("intInput");
},
}),
objectType({
name: "User",
definition(t) {
t.id("id", {
args: {
input: "InputObj",
},
});
},
}),
queryField("user", {
type: "User",
resolve: () => ({
id: `User:1`,
}),
}),
],
outputs: false,
shouldGenerateArtifacts: false,
});
expect(
await graphql(
schema,
`
{
user(input: { boolInput: true, floatInput: 123.4, intInput: 1 }) {
id
}
}
`
)
).toMatchSnapshot();
});
it("throws when chaining .list twice", () => {
expect(() => {
makeSchema({
types: [
inputObjectType({
name: "throwingListInput",
definition(t) {
t.list.list.id("id");
},
}),
],
outputs: false,
shouldGenerateArtifacts: false,
});
}).toThrowErrorMatchingSnapshot();
});
it("warns when specifying .list and list: true", () => {
const spy = jest.spyOn(console, "warn").mockImplementation();
makeSchema({
types: [
inputObjectType({
name: "throwingList",
definition(t) {
t.list.field("someField", {
list: true,
type: "Boolean",
});
},
}),
],
outputs: false,
shouldGenerateArtifacts: false,
});
expect(spy.mock.calls[0]).toMatchSnapshot();
expect(spy).toBeCalledTimes(1);
spy.mockRestore();
});
it("has asArg for using one-off inputObjects inline", async () => {
const schema = makeSchema({
types: [
objectType({
name: "User",
definition(t) {
t.id("id", {
args: {
input: inputObjectType({
name: "InputObj",
definition(t) {
t.id("idInput");
t.boolean("boolInput");
t.float("floatInput");
t.int("intInput");
},
}).asArg({ default: { idInput: 1 } }),
},
});
},
}),
queryField("user", {
type: "User",
resolve: () => ({
id: `User:1`,
}),
}),
],
outputs: false,
shouldGenerateArtifacts: false,
});
expect(
await graphql(
schema,
`
{
user(input: { boolInput: true, floatInput: 123.4, intInput: 1 }) {
id
}
}
`
)
).toMatchSnapshot();
});
});

View File

@ -0,0 +1,66 @@
import { graphql } from "graphql";
import path from "path";
import { interfaceType, makeSchema, objectType, queryField } from "../src/core";
describe("interfaceType", () => {
it("can be implemented by object types", async () => {
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" }),
}),
],
outputs: {
schema: path.join(__dirname, "interfaceTypeTest.graphql"),
typegen: false,
},
shouldGenerateArtifacts: false,
});
expect(
await graphql(
schema,
`
{
user {
id
name
}
}
`
)
).toMatchSnapshot();
});
it("logs error when resolveType is not provided for an interface", async () => {
const spy = jest.spyOn(console, "error").mockImplementation();
makeSchema({
types: [
interfaceType({
name: "Node",
definition(t) {
t.id("id");
},
}),
],
outputs: false,
shouldGenerateArtifacts: false,
});
expect(spy.mock.calls[0]).toMatchSnapshot();
expect(spy).toBeCalledTimes(1);
spy.mockRestore();
});
});

View File

@ -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,
});
});
});

View File

@ -11,24 +11,14 @@ describe("nonNullDefaults", () => {
output: true,
},
});
expect(printSchema(schema)).toMatchInlineSnapshot(`
"type Query {
test(test: Int!): Boolean!
}
"
`);
expect(printSchema(schema)).toMatchSnapshot();
});
test("true/true on type", () => {
const schema = makeSchema({
types: [makeQuery({ nonNullDefaults: { input: true, output: true } })],
outputs: false,
});
expect(printSchema(schema)).toMatchInlineSnapshot(`
"type Query {
test(test: Int!): Boolean!
}
"
`);
expect(printSchema(schema)).toMatchSnapshot();
});
test("false/false on schema", () => {
const schema = makeSchema({
@ -39,24 +29,14 @@ describe("nonNullDefaults", () => {
output: false,
},
});
expect(printSchema(schema)).toMatchInlineSnapshot(`
"type Query {
test(test: Int): Boolean
}
"
`);
expect(printSchema(schema)).toMatchSnapshot();
});
test("false/false on type", () => {
const schema = makeSchema({
types: [makeQuery({ nonNullDefaults: { input: false, output: false } })],
outputs: false,
});
expect(printSchema(schema)).toMatchInlineSnapshot(`
"type Query {
test(test: Int): Boolean
}
"
`);
expect(printSchema(schema)).toMatchSnapshot();
});
});

82
tests/objectType.spec.ts Normal file
View File

@ -0,0 +1,82 @@
import { graphql } from "graphql";
import { makeSchema, objectType, queryField } from "../src/core";
describe("objectType", () => {
it("builds creates an object type", async () => {
const schema = makeSchema({
types: [
objectType({
name: "User",
definition(t) {
t.id("id");
t.string("name");
t.float("floatField");
},
}),
queryField("user", {
type: "User",
resolve: () => ({
id: `User:1`,
name: "Test User",
floatField: 123.4,
}),
}),
],
outputs: false,
shouldGenerateArtifacts: false,
});
expect(
await graphql(
schema,
`
{
user {
id
name
floatField
}
}
`
)
).toMatchSnapshot();
});
it("throws when chaining .list twice", () => {
expect(() => {
makeSchema({
types: [
objectType({
name: "throwingList",
definition(t) {
t.list.list.id("id");
},
}),
],
outputs: false,
shouldGenerateArtifacts: false,
});
}).toThrowErrorMatchingSnapshot();
});
it("warns when specifying .list and list: true", () => {
const spy = jest.spyOn(console, "warn").mockImplementation();
makeSchema({
types: [
objectType({
name: "throwingList",
definition(t) {
t.list.field("someField", {
list: true,
type: "Boolean",
});
},
}),
],
outputs: false,
shouldGenerateArtifacts: false,
});
expect(spy.mock.calls[0]).toMatchSnapshot();
expect(spy).toBeCalledTimes(1);
spy.mockRestore();
});
});

26
tests/queryField.spec.ts Normal file
View File

@ -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,
});
});
});

59
tests/scalarType.spec.ts Normal file
View File

@ -0,0 +1,59 @@
import {
makeSchema,
objectType,
queryField,
asNexusMethod,
inputObjectType,
} from "../src/core";
import { graphql } from "graphql";
import { GraphQLDateTime, GraphQLDate } from "graphql-iso-date";
describe("scalarType", () => {
it("asNexusMethod: should wrap a scalar and make it available on the builder", async () => {
const schema = makeSchema({
types: [
asNexusMethod(GraphQLDateTime, "dateTime"),
asNexusMethod(GraphQLDate, "date"),
objectType({
name: "User",
definition(t) {
t.id("id");
// @ts-ignore
t.dateTime("dateTimeField");
},
}),
queryField("user", {
type: "User",
args: {
input: inputObjectType({
name: "SomeInput",
definition(t) {
// @ts-ignore
t.date("date", { required: true });
},
}).asArg({ required: true }),
},
resolve: (root, args) => ({
id: `User:1`,
dateTimeField: args.input.date,
}),
}),
],
outputs: false,
shouldGenerateArtifacts: false,
});
expect(
await graphql(
schema,
`
{
user(input: { date: "2020-01-01" }) {
id
dateTimeField
}
}
`
)
).toMatchSnapshot();
});
});

View File

@ -5,434 +5,18 @@ const { SDLConverter } = core;
describe("SDLConverter", () => {
test("printObjectTypes", () => {
expect(new SDLConverter(EXAMPLE_SDL).printObjectTypes())
.toMatchInlineSnapshot(`
"export const Mutation = objectType({
name: \\"Mutation\\",
definition(t) {
t.string(\\"someList\\", {
list: [false],
args: {
items: stringArg({
list: [false],
required: true
}),
},
})
t.field(\\"createPost\\", {
type: Post,
args: {
input: arg({
type: CreatePostInput,
required: true
}),
},
})
t.field(\\"registerClick\\", {
type: Query,
args: {
uuid: uuidArg(),
},
})
}
})
export const Post = objectType({
name: \\"Post\\",
description: \\"This is a description of a Post\\",
definition(t) {
t.implements(Node)
t.uuid(\\"uuid\\")
t.field(\\"author\\", { type: User })
t.float(\\"geo\\", { list: [true, true] })
t.float(\\"messyGeo\\", {
list: [true, false],
nullable: true,
})
}
})
export const Query = objectType({
name: \\"Query\\",
definition(t) {
t.field(\\"user\\", { type: User })
t.list.field(\\"posts\\", {
type: Post,
args: {
filters: arg({
type: PostFilters,
required: true
}),
},
})
t.field(\\"unionField\\", { type: ExampleUnion })
}
})
export const User = objectType({
name: \\"User\\",
definition(t) {
t.implements(Node)
t.string(\\"name\\", {
description: \\"This is a description of a name\\",
args: {
prefix: stringArg({ description: \\"And a description of an arg\\" }),
},
})
t.string(\\"email\\")
t.string(\\"phone\\", { nullable: true })
t.list.field(\\"posts\\", {
type: Post,
args: {
filters: arg({ type: PostFilters }),
},
})
t.field(\\"outEnum\\", {
type: SomeEnum,
nullable: true,
})
}
})"
`);
expect(new SDLConverter(EXAMPLE_SDL).printObjectTypes()).toMatchSnapshot();
});
test("printEnumTypes", () => {
expect(new SDLConverter(EXAMPLE_SDL).printEnumTypes())
.toMatchInlineSnapshot(`
"export const OrderEnum = enumType({
name: \\"OrderEnum\\",
members: [\\"ASC\\",\\"DESC\\"],
});
export const SomeEnum = enumType({
name: \\"SomeEnum\\",
members: [\\"A\\",{\\"name\\":\\"B\\",\\"deprecation\\":\\"This is a deprecation reason for B\\",\\"value\\":\\"B\\"}],
});"
`);
});
test("printScalarTypes", () => {
expect(new SDLConverter(EXAMPLE_SDL).printScalarTypes())
.toMatchInlineSnapshot(`
"export const UUID = scalarType({
name: \\"UUID\\",
asNexusMethod: \\"uuid\\",
serialize() { /* Todo */ },
parseValue() { /* Todo */ },
parseLiteral() { /* Todo */ }
});"
`);
});
test("printInterfaceTypes", () => {
expect(new SDLConverter(EXAMPLE_SDL).printInterfaceTypes())
.toMatchInlineSnapshot(`
"export const Node = interfaceType({
name: \\"Node\\",
description: \\"This is a description of a Node\\",
definition(t) {
t.id(\\"id\\")
t.resolveType(() => null)
}
});"
`);
expect(new SDLConverter(EXAMPLE_SDL).printEnumTypes()).toMatchSnapshot();
});
});
test("convertSDL", () => {
expect(convertSDL(EXAMPLE_SDL)).toMatchInlineSnapshot(`
"import { objectType, stringArg, arg, uuidArg, interfaceType, inputObjectType, unionType, enumType, scalarType } from 'nexus';
export const Mutation = objectType({
name: \\"Mutation\\",
definition(t) {
t.string(\\"someList\\", {
list: [false],
args: {
items: stringArg({
list: [false],
required: true
}),
},
})
t.field(\\"createPost\\", {
type: Post,
args: {
input: arg({
type: CreatePostInput,
required: true
}),
},
})
t.field(\\"registerClick\\", {
type: Query,
args: {
uuid: uuidArg(),
},
})
}
})
export const Post = objectType({
name: \\"Post\\",
description: \\"This is a description of a Post\\",
definition(t) {
t.implements(Node)
t.uuid(\\"uuid\\")
t.field(\\"author\\", { type: User })
t.float(\\"geo\\", { list: [true, true] })
t.float(\\"messyGeo\\", {
list: [true, false],
nullable: true,
})
}
})
export const Query = objectType({
name: \\"Query\\",
definition(t) {
t.field(\\"user\\", { type: User })
t.list.field(\\"posts\\", {
type: Post,
args: {
filters: arg({
type: PostFilters,
required: true
}),
},
})
t.field(\\"unionField\\", { type: ExampleUnion })
}
})
export const User = objectType({
name: \\"User\\",
definition(t) {
t.implements(Node)
t.string(\\"name\\", {
description: \\"This is a description of a name\\",
args: {
prefix: stringArg({ description: \\"And a description of an arg\\" }),
},
})
t.string(\\"email\\")
t.string(\\"phone\\", { nullable: true })
t.list.field(\\"posts\\", {
type: Post,
args: {
filters: arg({ type: PostFilters }),
},
})
t.field(\\"outEnum\\", {
type: SomeEnum,
nullable: true,
})
}
})
export const Node = interfaceType({
name: \\"Node\\",
description: \\"This is a description of a Node\\",
definition(t) {
t.id(\\"id\\")
t.resolveType(() => null)
}
});
export const CreatePostInput = inputObjectType({
name: \\"CreatePostInput\\",
definition(t) {
t.string(\\"name\\", { required: true })
t.id(\\"author\\", { required: true })
t.float(\\"geo\\", {
list: [false, true],
required: true,
})
}
});
export const PostFilters = inputObjectType({
name: \\"PostFilters\\",
definition(t) {
t.field(\\"order\\", {
type: OrderEnum,
required: true,
})
t.string(\\"search\\")
}
});
export const ExampleUnion = unionType({
name: \\"ExampleUnion\\",
definition(t) {
t.members(Post, User)
}
});
export const OrderEnum = enumType({
name: \\"OrderEnum\\",
members: [\\"ASC\\",\\"DESC\\"],
});
export const SomeEnum = enumType({
name: \\"SomeEnum\\",
members: [\\"A\\",{\\"name\\":\\"B\\",\\"deprecation\\":\\"This is a deprecation reason for B\\",\\"value\\":\\"B\\"}],
});
export const UUID = scalarType({
name: \\"UUID\\",
asNexusMethod: \\"uuid\\",
serialize() { /* Todo */ },
parseValue() { /* Todo */ },
parseLiteral() { /* Todo */ }
});"
`);
expect(convertSDL(EXAMPLE_SDL)).toMatchSnapshot();
});
test("convertSDL as commonjs", () => {
expect(convertSDL(EXAMPLE_SDL, true)).toMatchInlineSnapshot(`
"const { objectType, stringArg, arg, uuidArg, interfaceType, inputObjectType, unionType, enumType, scalarType } = require('nexus');
const Mutation = objectType({
name: \\"Mutation\\",
definition(t) {
t.string(\\"someList\\", {
list: [false],
args: {
items: stringArg({
list: [false],
required: true
}),
},
})
t.field(\\"createPost\\", {
type: Post,
args: {
input: arg({
type: CreatePostInput,
required: true
}),
},
})
t.field(\\"registerClick\\", {
type: Query,
args: {
uuid: uuidArg(),
},
})
}
})
const Post = objectType({
name: \\"Post\\",
description: \\"This is a description of a Post\\",
definition(t) {
t.implements(Node)
t.uuid(\\"uuid\\")
t.field(\\"author\\", { type: User })
t.float(\\"geo\\", { list: [true, true] })
t.float(\\"messyGeo\\", {
list: [true, false],
nullable: true,
})
}
})
const Query = objectType({
name: \\"Query\\",
definition(t) {
t.field(\\"user\\", { type: User })
t.list.field(\\"posts\\", {
type: Post,
args: {
filters: arg({
type: PostFilters,
required: true
}),
},
})
t.field(\\"unionField\\", { type: ExampleUnion })
}
})
const User = objectType({
name: \\"User\\",
definition(t) {
t.implements(Node)
t.string(\\"name\\", {
description: \\"This is a description of a name\\",
args: {
prefix: stringArg({ description: \\"And a description of an arg\\" }),
},
})
t.string(\\"email\\")
t.string(\\"phone\\", { nullable: true })
t.list.field(\\"posts\\", {
type: Post,
args: {
filters: arg({ type: PostFilters }),
},
})
t.field(\\"outEnum\\", {
type: SomeEnum,
nullable: true,
})
}
})
const Node = interfaceType({
name: \\"Node\\",
description: \\"This is a description of a Node\\",
definition(t) {
t.id(\\"id\\")
t.resolveType(() => null)
}
});
const CreatePostInput = inputObjectType({
name: \\"CreatePostInput\\",
definition(t) {
t.string(\\"name\\", { required: true })
t.id(\\"author\\", { required: true })
t.float(\\"geo\\", {
list: [false, true],
required: true,
})
}
});
const PostFilters = inputObjectType({
name: \\"PostFilters\\",
definition(t) {
t.field(\\"order\\", {
type: OrderEnum,
required: true,
})
t.string(\\"search\\")
}
});
const ExampleUnion = unionType({
name: \\"ExampleUnion\\",
definition(t) {
t.members(Post, User)
}
});
const OrderEnum = enumType({
name: \\"OrderEnum\\",
members: [\\"ASC\\",\\"DESC\\"],
});
const SomeEnum = enumType({
name: \\"SomeEnum\\",
members: [\\"A\\",{\\"name\\":\\"B\\",\\"deprecation\\":\\"This is a deprecation reason for B\\",\\"value\\":\\"B\\"}],
});
const UUID = scalarType({
name: \\"UUID\\",
asNexusMethod: \\"uuid\\",
serialize() { /* Todo */ },
parseValue() { /* Todo */ },
parseLiteral() { /* Todo */ }
});
exports.Mutation = Mutation;
exports.Post = Post;
exports.Query = Query;
exports.User = User;
exports.Node = Node;
exports.CreatePostInput = CreatePostInput;
exports.PostFilters = PostFilters;
exports.ExampleUnion = ExampleUnion;
exports.OrderEnum = OrderEnum;
exports.SomeEnum = SomeEnum;
exports.UUID = UUID;"
`);
expect(convertSDL(EXAMPLE_SDL, true)).toMatchSnapshot();
});

View File

@ -0,0 +1,44 @@
import { makeSchema, subscriptionField } from "../src/core";
describe("subscriptionField", () => {
// TODO: Actually validate real subscription usage
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,
});
});
});

7
tests/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["node", "jest"],
"noUnusedLocals": false
}
}

82
tests/unionType.spec.ts Normal file
View File

@ -0,0 +1,82 @@
import { graphql } from "graphql";
import path from "path";
import fs from "fs";
import {
makeSchema,
objectType,
queryField,
unionType,
} from "../src/core";
describe("unionType", () => {
test("unionType", async () => {
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"),
}),
}),
],
outputs: {
schema: path.join(__dirname, "unionTypeTest.graphql"),
typegen: false,
},
shouldGenerateArtifacts: false,
});
expect(
await graphql(
schema,
`
fragment UserOrErrorFields on UserOrError {
__typename
... on User {
id
name
}
... on DeletedUser {
message
}
}
query UserOrErrorTest {
userTest {
...UserOrErrorFields
}
deletedUserTest {
...UserOrErrorFields
}
}
`
)
).toMatchSnapshot();
});
});

View File

@ -1,4 +1,5 @@
import { objectType, makeSchemaInternal, makeSchema } from "../src/core";
import { objectType, makeSchemaInternal, makeSchema, UNKNOWN_TYPE_SCALAR } from "../src/core";
import { Kind } from "graphql";
describe("unknownType", () => {
const Query = objectType({
@ -59,10 +60,23 @@ describe("unknownType", () => {
shouldGenerateArtifacts: true,
});
} catch (e) {
expect(e).toMatchInlineSnapshot(`
[Error:
- Missing type User, did you forget to import a type to the root query?]
`);
expect(e).toMatchSnapshot()
}
});
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."
);
});
});

View File

@ -9,12 +9,11 @@
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"declaration": true,
"importHelpers": true,
"resolveJsonModule": true,
"sourceMap": true,
"declarationMap": true
"types": ["node"]
},
"exclude": [
"./examples",

View File

@ -325,6 +325,13 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/graphql-iso-date@^3.3.3":
version "3.3.3"
resolved "https://registry.yarnpkg.com/@types/graphql-iso-date/-/graphql-iso-date-3.3.3.tgz#a368aa7370512a9cc87a5035c3701949ed7db9e2"
integrity sha512-lchvlAox/yqk2Rcrgqh+uvwc1UC9i1hap+0tqQqyYYcAica6Uw2D4mUkCNcw+WeZ8dvSS5QdtIlJuDYUf4nLXQ==
dependencies:
graphql "^14.5.3"
"@types/graphql@14.0.7":
version "14.0.7"
resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.0.7.tgz#daa09397220a68ce1cbb3f76a315ff3cd92312f6"
@ -1432,6 +1439,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.1.tgz#1c1f0c364882c868f5bff6512146328336a11b1d"
integrity sha512-b9usnbDGnD928gJB3LrCmxoibr3VE4U2SMo5PBuBnokWyDADTqDPXg4YpwKF1trpH+UbGp7QLicO3+aWEy0+mw==
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.0.2:
version "14.4.2"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.4.2.tgz#553a7d546d524663eda49ed6df77577be3203ae3"
@ -1439,6 +1451,13 @@ graphql@^14.0.2:
dependencies:
iterall "^1.2.2"
graphql@^14.5.3:
version "14.5.7"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.7.tgz#8646a3fcc07922319cc3967eba4a64b32929f77f"
integrity sha512-as410RMJSUFqF8RcH2QWxZ5ioqHzsH9VWnWbaU+UnDXJ/6azMDIYPrtXCBPXd8rlunEVb7W8z6fuUnNHMbFu9A==
dependencies:
iterall "^1.2.2"
growly@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"