fix: subscription type static typings (#564)

fixes #559
This commit is contained in:
Jason Kuhrt 2020-10-20 11:13:08 -04:00 committed by GitHub
parent e65f6ef989
commit 2edfcfa629
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 230 additions and 140 deletions

View File

@ -13,7 +13,7 @@
"trailingComma": "all"
},
"dependencies": {
"@nexus/schema": "^0.16.0",
"@nexus/schema": "0.17.0-next.2",
"graphql": "^15.3.0"
},
"devDependencies": {

View File

@ -2,10 +2,10 @@
# yarn lockfile v1
"@nexus/schema@^0.16.0":
version "0.16.0"
resolved "https://registry.yarnpkg.com/@nexus/schema/-/schema-0.16.0.tgz#cbf2b70eb47a854e4bcec849583368d7e7048087"
integrity sha512-H+JJDycjcilvsRtLySpwfVXp1gYiOqPPLQdq04R4Vp5S400PpdADQlKMV4EvpBJ/BCxwQ8XwP8+nrOKvDZLLUw==
"@nexus/schema@0.17.0-next.2":
version "0.17.0-next.2"
resolved "https://registry.yarnpkg.com/@nexus/schema/-/schema-0.17.0-next.2.tgz#b85fcb1cd35d4fd65618404f406d0e1e309ffda7"
integrity sha512-EMUYhEvo6DkbZSBC/ErlSN59KWDVrZbuBbRY+QbRn2EsOcOhA3Y6g+/SDYEfJwqJzKjiC9AjqOmOqzmBXBNJQA==
dependencies:
iterall "^1.2.2"
tslib "^1.9.3"

View File

@ -1,21 +1,18 @@
const path = require("path");
const path = require('path')
/**
* @type {jest.InitialOptions}
*/
module.exports = {
setupFilesAfterEnv: ["<rootDir>/tests/_setup.ts"],
preset: "ts-jest",
testEnvironment: "node",
watchPlugins: [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname",
],
setupFilesAfterEnv: ['<rootDir>/tests/_setup.ts'],
preset: 'ts-jest',
testEnvironment: 'node',
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
globals: {
"ts-jest": {
'ts-jest': {
diagnostics: true, // (temp) true
tsConfig: path.join(__dirname, "tests/tsconfig.json"),
tsConfig: path.join(__dirname, 'tests/tsconfig.json'),
},
},
collectCoverageFrom: ["src/**/*"],
};
collectCoverageFrom: ['src/**/*'],
}

View File

@ -64,28 +64,24 @@ export type NexusOutputFieldDef = NexusOutputFieldConfig<string, any> & {
subscribe?: GraphQLFieldResolver<any, any>
}
/**
* Ensure type-safety by checking
*/
export type ScalarOutSpread<TypeName extends string, FieldName extends string> = NeedsResolver<
TypeName,
FieldName
> extends true
? HasGen3<'argTypes', TypeName, FieldName> extends true
? [ScalarOutConfig<TypeName, FieldName>]
: [ScalarOutConfig<TypeName, FieldName>] | [FieldResolver<TypeName, FieldName>]
: HasGen3<'argTypes', TypeName, FieldName> extends true
? [ScalarOutConfig<TypeName, FieldName>]
: [] | [FieldResolver<TypeName, FieldName>] | [ScalarOutConfig<TypeName, FieldName>]
// prettier-ignore
export type ScalarOutSpread<TypeName extends string, FieldName extends string> =
NeedsResolver<TypeName, FieldName> extends true
? HasGen3<'argTypes', TypeName, FieldName> extends true
? [ScalarOutConfig<TypeName, FieldName>]
: [ScalarOutConfig<TypeName, FieldName>] | [FieldResolver<TypeName, FieldName>]
: HasGen3<'argTypes', TypeName, FieldName> extends true
? [ScalarOutConfig<TypeName, FieldName>]
: [] | [FieldResolver<TypeName, FieldName>] | [ScalarOutConfig<TypeName, FieldName>]
export type ScalarOutConfig<TypeName extends string, FieldName extends string> = NeedsResolver<
TypeName,
FieldName
> extends true
? OutputScalarConfig<TypeName, FieldName> & {
resolve: FieldResolver<TypeName, FieldName>
}
: OutputScalarConfig<TypeName, FieldName>
// prettier-ignore
export type ScalarOutConfig<TypeName extends string, FieldName extends string> =
NeedsResolver<TypeName, FieldName> extends true
? OutputScalarConfig<TypeName, FieldName> &
{
resolve: FieldResolver<TypeName, FieldName>
}
: OutputScalarConfig<TypeName, FieldName>
export type FieldOutConfig<TypeName extends string, FieldName extends string> = NeedsResolver<
TypeName,
@ -110,9 +106,11 @@ export interface InputDefinitionBuilder {
warn(msg: string): void
}
// prettier-ignore
export interface OutputDefinitionBlock<TypeName extends string>
extends NexusGenCustomOutputMethods<TypeName>,
NexusGenCustomOutputProperties<TypeName> {}
extends NexusGenCustomOutputMethods<TypeName>,
NexusGenCustomOutputProperties<TypeName>
{}
/**
* The output definition block is passed to the "definition"
@ -158,8 +156,7 @@ export class OutputDefinitionBlock<TypeName extends string> {
// 2. NexusOutputFieldDef is contrained to be be a string
// 3. so `name` is not compatible
// 4. and changing FieldOutConfig to FieldOutConfig<string breaks types in other places
const field: any = { name, ...fieldConfig }
this.typeBuilder.addField(this.decorateField(field))
this.typeBuilder.addField(this.decorateField({ name, ...fieldConfig } as any))
}
protected addScalarField(

View File

@ -5,7 +5,10 @@ import { NexusTypes, withNexusSymbol } from './_types'
export interface NexusExtendTypeConfig<TypeName extends string> {
type: TypeName
definition(t: OutputDefinitionBlock<TypeName>): void
definition(
// t: IsSubscriptionType<TypeName> extends true ? SubscriptionBuilder : OutputDefinitionBlock<TypeName>
t: OutputDefinitionBlock<TypeName>
): void
}
export class NexusExtendTypeDef<TypeName extends string> {

View File

@ -14,7 +14,7 @@ export function subscriptionField<FieldName extends string>(
type: 'Subscription',
definition(t) {
const finalConfig = typeof config === 'function' ? config() : config
t.field(fieldName, finalConfig)
t.field(fieldName, finalConfig as any)
},
})
}

View File

@ -1,63 +1,15 @@
import { GraphQLResolveInfo } from 'graphql'
import {
ArgsValue,
FieldResolver,
GetGen,
HasGen3,
MaybePromise,
MaybePromiseDeep,
NeedsResolver,
ResultValue,
} from '../typegenTypeHelpers'
import { CommonOutputFieldConfig } from './definitionBlocks'
import { ObjectDefinitionBlock, ObjectDefinitionBuilder, objectType } from './objectType'
import { ArgsValue, GetGen, MaybePromise, MaybePromiseDeep, ResultValue } from '../typegenTypeHelpers'
import { IsEqual } from '../utils'
import { CommonOutputFieldConfig, NexusOutputFieldDef } from './definitionBlocks'
import { ObjectDefinitionBuilder, objectType } from './objectType'
import { AllNexusOutputTypeDefs } from './wrapping'
import { BaseScalars } from './_types'
export interface SubscriptionScalarConfig<TypeName extends string, FieldName extends string, T = any>
extends CommonOutputFieldConfig<TypeName, FieldName> {
/**
* Resolve method for the field
*/
resolve?: FieldResolver<TypeName, FieldName>
subscribe(
root: object,
args: ArgsValue<TypeName, FieldName>,
ctx: GetGen<'context'>,
info: GraphQLResolveInfo
): MaybePromise<AsyncIterator<T>> | MaybePromiseDeep<AsyncIterator<T>>
}
export type ScalarSubSpread<TypeName extends string, FieldName extends string> = NeedsResolver<
TypeName,
FieldName
> extends true
? HasGen3<'argTypes', TypeName, FieldName> extends true
? [ScalarSubConfig<TypeName, FieldName>]
: [ScalarSubConfig<TypeName, FieldName>] | [FieldResolver<TypeName, FieldName>]
: HasGen3<'argTypes', TypeName, FieldName> extends true
? [ScalarSubConfig<TypeName, FieldName>]
: [] | [FieldResolver<TypeName, FieldName>] | [ScalarSubConfig<TypeName, FieldName>]
export type ScalarSubConfig<TypeName extends string, FieldName extends string> = NeedsResolver<
TypeName,
FieldName
> extends true
? SubscriptionScalarConfig<TypeName, FieldName> & {
resolve: FieldResolver<TypeName, FieldName>
}
: SubscriptionScalarConfig<TypeName, FieldName>
export interface SubscribeFieldConfig<TypeName extends string, FieldName extends string, T = any>
extends CommonOutputFieldConfig<TypeName, FieldName> {
type: GetGen<'allOutputTypes'> | AllNexusOutputTypeDefs
/**
* Resolve method for the field
*/
export interface SubscribeFieldConfigBase<FieldName extends string, Event = any> {
resolve(
root: T,
args: ArgsValue<TypeName, FieldName>,
root: Event,
args: ArgsValue<'Subscription', FieldName>,
context: GetGen<'context'>,
info: GraphQLResolveInfo
):
@ -66,66 +18,94 @@ export interface SubscribeFieldConfig<TypeName extends string, FieldName extends
subscribe(
root: object,
args: ArgsValue<TypeName, FieldName>,
args: ArgsValue<'Subscription', FieldName>,
ctx: GetGen<'context'>,
info: GraphQLResolveInfo
): MaybePromise<AsyncIterator<T>> | MaybePromiseDeep<AsyncIterator<T>>
): MaybePromise<AsyncIterator<Event>> | MaybePromiseDeep<AsyncIterator<Event>>
}
export interface SubscriptionDefinitionBuilder extends ObjectDefinitionBuilder<'Subscription'> {}
// prettier-ignore
export type FieldShorthandConfig<FieldName extends string> =
CommonOutputFieldConfig<'Subscription', FieldName>
& SubscribeFieldConfigBase<FieldName>
export class SubscriptionDefinitionBlock extends ObjectDefinitionBlock<'Subscription'> {
constructor(protected typeBuilder: SubscriptionDefinitionBuilder, protected isList = false) {
super(typeBuilder)
// prettier-ignore
export interface SubscribeFieldConfig<TypeName extends string, FieldName extends string>
extends SubscribeFieldConfigBase<FieldName>,
CommonOutputFieldConfig<'Subscription', FieldName>
{
type: GetGen<'allOutputTypes'> | AllNexusOutputTypeDefs
}
export interface SubscriptionBuilderInternal extends ObjectDefinitionBuilder<'Subscription'> {}
export class SubscriptionBuilder {
constructor(protected typeBuilder: SubscriptionBuilderInternal, protected isList = false) {}
get list() {
if (this.isList) {
throw new Error('Cannot chain list.list, in the definition block. Use `list: []` config value')
}
return new SubscriptionDefinitionBlock(this.typeBuilder, true)
return new SubscriptionBuilder(this.typeBuilder, true)
}
string<FieldName extends string>(
fieldName: FieldName,
...opts: ScalarSubSpread<'Subscription', FieldName>
) {
this.addScalarField(fieldName, 'String', opts)
string<FieldName extends string>(fieldName: FieldName, config: FieldShorthandConfig<FieldName>) {
this.fieldShorthand(fieldName, 'String', config)
}
int<FieldName extends string>(fieldName: FieldName, ...opts: ScalarSubSpread<'Subscription', FieldName>) {
this.addScalarField(fieldName, 'Int', opts)
int<FieldName extends string>(fieldName: FieldName, config: FieldShorthandConfig<FieldName>) {
this.fieldShorthand(fieldName, 'Int', config)
}
boolean<FieldName extends string>(
fieldName: FieldName,
...opts: ScalarSubSpread<'Subscription', FieldName>
) {
this.addScalarField(fieldName, 'Boolean', opts)
// prettier-ignore
boolean<FieldName extends string>(fieldName: FieldName, opts: FieldShorthandConfig<FieldName>) {
this.fieldShorthand(fieldName, 'Boolean', opts)
}
id<FieldName extends string>(fieldName: FieldName, ...opts: ScalarSubSpread<'Subscription', FieldName>) {
this.addScalarField(fieldName, 'ID', opts)
id<FieldName extends string>(fieldName: FieldName, config: FieldShorthandConfig<FieldName>) {
this.fieldShorthand(fieldName, 'ID', config)
}
float<FieldName extends string>(fieldName: FieldName, ...opts: ScalarSubSpread<'Subscription', FieldName>) {
this.addScalarField(fieldName, 'Float', opts)
float<FieldName extends string>(fieldName: FieldName, config: FieldShorthandConfig<FieldName>) {
this.fieldShorthand(fieldName, 'Float', config)
}
field<FieldName extends string>(
name: FieldName,
fieldConfig: SubscribeFieldConfig<'Subscription', FieldName>
) {
const field: any = { name, ...fieldConfig }
this.typeBuilder.addField(this.decorateField(field))
// prettier-ignore
field<FieldName extends string>(name: FieldName, fieldConfig: SubscribeFieldConfig<'Subscription', FieldName>) {
this.typeBuilder.addField(this.decorateField({ name, ...fieldConfig } as any))
}
protected fieldShorthand(fieldName: string, typeName: BaseScalars, config: FieldShorthandConfig<any>) {
this.typeBuilder.addField(
this.decorateField({
name: fieldName,
type: typeName,
...config,
} as any)
)
}
protected decorateField(config: NexusOutputFieldDef): NexusOutputFieldDef {
if (this.isList) {
if (config.list) {
this.typeBuilder.warn(
`It looks like you chained .list and set list for ${config.name}. ` +
'You should only do one or the other'
)
} else {
config.list = true
}
}
return config
}
}
export type NexusSubscriptionTypeConfig = {
name: 'Subscription'
definition(t: SubscriptionDefinitionBlock): void
export type SubscriptionTypeParams = {
definition(t: SubscriptionBuilder): void
}
export function subscriptionType(config: Omit<NexusSubscriptionTypeConfig, 'name'>) {
return objectType({ name: 'Subscription', ...config })
export function subscriptionType(config: SubscriptionTypeParams) {
return objectType({ name: 'Subscription', ...config } as any)
}
export type IsSubscriptionType<T> = IsEqual<T, 'Subscription'>

View File

@ -415,3 +415,8 @@ export function getOwnPackage(): { name: string } {
export function casesHandled(x: never): never {
throw new Error(`A case was not handled for value: ${x}`)
}
/**
* Is the given type equal to the other given type?
*/
export type IsEqual<A, B> = A extends B ? (B extends A ? true : false) : false

View File

@ -9,7 +9,9 @@ import {
objectType,
queryType,
stringArg,
subscriptionType,
} from '../../src'
import { mockStream } from '../_helpers'
import './_app.typegen'
export const query = queryType({
@ -96,3 +98,76 @@ export const Mutation = mutationType({
})
},
})
export const Subscription = subscriptionType({
definition(t) {
// lists
t.list.field('someFields', {
type: 'Int',
subscribe() {
return mockStream(10, 0, (int) => int - 1)
},
resolve: (event) => {
return event
},
})
t.list.int('someInts', {
subscribe() {
return mockStream(10, 0, (int) => int + 1)
},
resolve: (event) => {
return event
},
})
// singular
t.field('someField', {
type: 'Int',
subscribe() {
return mockStream(10, 0, (int) => int - 1)
},
resolve: (event) => {
return event
},
})
t.int('someInt', {
subscribe() {
return mockStream(10, 0, (int) => int + 1)
},
resolve: (event) => {
return event
},
})
t.string('someString', {
subscribe() {
return mockStream(10, '', (str) => str + '!')
},
resolve: (event) => {
return event
},
})
t.float('someFloat', {
subscribe() {
return mockStream(10, 0.5, (f) => f)
},
resolve: (event) => {
return event
},
})
t.boolean('someBoolean', {
subscribe() {
return mockStream(10, true, (b) => b)
},
resolve: (event) => {
return event
},
})
t.id('someID', {
subscribe() {
return mockStream(10, 'abc', (id) => id)
},
resolve: (event) => {
return event
},
})
},
})

View File

@ -50,6 +50,7 @@ export interface NexusGenRootTypes {
title?: string | null // String
}
Query: {}
Subscription: {}
User: { firstName: string; lastName: string }
}
@ -78,6 +79,17 @@ export interface NexusGenFieldTypes {
searchPosts: Array<NexusGenRootTypes['Post'] | null> | null // [Post]
user: NexusGenRootTypes['User'] | null // User
}
Subscription: {
// field return type
someBoolean: boolean | null // Boolean
someField: number | null // Int
someFields: Array<number | null> | null // [Int]
someFloat: number | null // Float
someID: string | null // ID
someInt: number | null // Int
someInts: Array<number | null> | null // [Int]
someString: string | null // String
}
User: {
// field return type
firstName: string | null // String
@ -109,7 +121,7 @@ export interface NexusGenAbstractResolveReturnTypes {}
export interface NexusGenInheritedFields {}
export type NexusGenObjectNames = 'Mutation' | 'Post' | 'Query' | 'User'
export type NexusGenObjectNames = 'Mutation' | 'Post' | 'Query' | 'Subscription' | 'User'
export type NexusGenInputNames = 'PostSearchInput'

View File

@ -1,5 +1,5 @@
import { buildSchema, graphql, GraphQLSchema, printSchema, introspectionFromSchema } from 'graphql'
import { makeSchema, MiddlewareFn, objectType, plugin, queryField, interfaceType } from '../src/core'
import { buildSchema, graphql, GraphQLSchema, printSchema } from 'graphql'
import { interfaceType, makeSchema, MiddlewareFn, objectType, plugin, queryField } from '../src/core'
import { nullabilityGuardPlugin } from '../src/plugins'
import { EXAMPLE_SDL } from './_sdl'
@ -126,7 +126,7 @@ describe('plugin', () => {
if (!exec) {
return null
}
const [match, typeName, fieldType] = exec
const [, typeName, fieldType] = exec
switch (fieldType) {
case 'Edge': {
return objectType({

View File

@ -398,7 +398,7 @@ describe('nullabilityGuardPlugin', () => {
})
it('will still fail if it cant handle with a guard', async () => {
const { errors = [], data } = await graphql(
const { errors = [] } = await graphql(
defaultSchema,
`
{

View File

@ -88,7 +88,7 @@ describe('queryComplexityPlugin', () => {
})
test('throws error if complexity is of invalid type', () => {
const testSchema = createTestSchema([
createTestSchema([
objectType({
name: 'User',
definition(t) {

View File

@ -7,6 +7,25 @@ it('defines a field on the mutation type as shorthand', async () => {
types: [
subscriptionType({
definition(t) {
// lists
t.list.field('someFields', {
type: 'Int',
subscribe() {
return mockStream(10, 0, (int) => int - 1)
},
resolve: (event) => {
return event
},
})
t.list.int('someInts', {
subscribe() {
return mockStream(10, 0, (int) => int + 1)
},
resolve: (event) => {
return event
},
})
// singular
t.field('someField', {
type: 'Int',
subscribe() {
@ -64,6 +83,8 @@ it('defines a field on the mutation type as shorthand', async () => {
expect(GQL.printSchema(schema)).toMatchInlineSnapshot(`
"type Subscription {
someFields: [Int]
someInts: [Int]
someField: Int
someInt: Int
someString: String