├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── docs └── .gitkeep ├── package-lock.json ├── package.json ├── src ├── definition │ ├── GraphQLTypeIndex.ts │ ├── InputType.ts │ ├── InterfaceType.ts │ ├── ObjectType.ts │ ├── SubscriptionType.ts │ ├── rootTypes.ts │ └── scalar.ts ├── global.ts ├── index.ts ├── metadata │ ├── Definition.ts │ ├── MetadataStorage.ts │ └── Reference.ts ├── reference │ ├── Field.ts │ ├── Implement.ts │ └── InputField.ts ├── relay │ ├── connection.ts │ ├── mutation.ts │ └── node.ts ├── sdl │ ├── ast.ts │ └── gql.ts ├── type-expression │ ├── DefinitionTypeExpression.ts │ ├── GraphQLTypeExpression.ts │ ├── TypeExpression.ts │ ├── coerceType.ts │ ├── structure.ts │ └── types.ts ├── types.ts └── utilities │ ├── ResolverContext.ts │ └── formatObjectInfo.ts ├── tests ├── __snapshots__ │ ├── basic_mutation.test.ts.snap │ ├── basic_query.test.ts.snap │ ├── field_with_graphqltype.test.ts.snap │ ├── input_type.test.ts.snap │ ├── recursive_type.test.ts.snap │ └── root_types_and_extensions.test.ts.snap ├── basic_mutation.test.ts ├── basic_query.test.ts ├── basic_subscriptions.test.ts ├── field_with_graphqltype.test.ts ├── input_type.test.ts ├── plain_resolvermap.test.ts ├── recursive_type.test.ts ├── root_types_and_extensions.test.ts ├── starwars-relay │ ├── ArrayConnection.ts │ ├── starWarsConnection.test.ts │ ├── starWarsData.ts │ ├── starWarsMutation.test.ts │ ├── starWarsObjectIdentification.test.ts │ └── starWarsSchema.ts ├── starwars │ ├── starWarsData.ts │ ├── starWarsIntrospection.test.ts │ ├── starWarsQuery.test.ts │ ├── starWarsSchema.ts │ └── starWarsValidation.test.ts ├── typeutils.test.ts └── utilities.test.ts ├── tsconfig.base.json ├── tsconfig.build.json ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | /temp/ 4 | 5 | # Coverage 6 | coverage/ 7 | 8 | # Ignore the compiled output. 9 | /lib 10 | 11 | # Temporary tar files for publishing 12 | /*.tgz 13 | 14 | # Logs 15 | logs 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | *.tsbuildinfo 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | node_js: 4 | - '14' 5 | - '13' 6 | - '12' 7 | - '11' 8 | - '10' 9 | 10 | install: 11 | - npm install 12 | 13 | script: 14 | - npm run lint 15 | - npm run test:ci 16 | - npm run coverage:upload 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Girin Tests", 11 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 12 | "cwd": "${workspaceRoot}", 13 | "args": [ 14 | "-i", 15 | "${relativeFile}", 16 | ], 17 | "internalConsoleOptions": "openOnSessionStart", 18 | "outFiles": [ 19 | "${workspaceRoot}/dist/**/*" 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.preferences.quoteStyle": "single" 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright Max Choi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Girin 🦒 2 | 3 | [![npm version](https://badge.fury.io/js/girin.svg)](https://badge.fury.io/js/girin) 4 | [![Build Status](https://travis-ci.org/hanpama/girin.svg?branch=master)](https://travis-ci.org/hanpama/girin) 5 | [![codecov](https://codecov.io/gh/hanpama/girin/branch/master/graph/badge.svg)](https://codecov.io/gh/hanpama/girin) 6 | 7 | 8 | ## Development 9 | 10 | ### Initialization 11 | 12 | ```sh 13 | # 1. clone the repository 14 | git clone https://github.com/hanpama/girin 15 | 16 | # 2. cd into it 17 | cd girin 18 | 19 | # 3. install dependency and link packages 20 | npm run bootstrap 21 | 22 | # 4. run typescript compiler server 23 | npm run watch 24 | ``` 25 | 26 | ### Testing and Debugging 27 | 28 | ``` 29 | npm test 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanpama/girin-ts/80c6bb2b5b1b0f4df380943a69b3323ab1dfd26a/docs/.gitkeep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "girin", 3 | "version": "0.6.1", 4 | "description": "Define GraphQL types linked to classes", 5 | "author": "Max Choi ", 6 | "main": "./lib/index.js", 7 | "types": "./lib/index.d.ts", 8 | "files": [ 9 | "lib", 10 | "src" 11 | ], 12 | "scripts": { 13 | "build": "tsc -p tsconfig.build.json", 14 | "prepublish": "npm run build", 15 | "test": "jest --verbose", 16 | "test:ci": "npm run coverage -- --ci", 17 | "testonly": "npm test", 18 | "coverage": "npm test -- --coverage", 19 | "coverage:upload": "codecov", 20 | "lint": "tslint --project tslint.json src/**/*.ts" 21 | }, 22 | "tags": [ 23 | "graphql", 24 | "typescript" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/hanpama/girin-ts.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/hanpama/girin-ts/issues" 32 | }, 33 | "homepage": "https://github.com/hanpama/girin-ts", 34 | "license": "MIT", 35 | "peerDependencies": { 36 | "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" 37 | }, 38 | "devDependencies": { 39 | "@types/graphql-relay": "^0.7.0", 40 | "@types/jest": "^29.5.12", 41 | "codecov": "^3.8.3", 42 | "cursor-connection": "^0.5.2", 43 | "graphql": "^16.9.0", 44 | "graphql-relay": "^0.10.2", 45 | "jest": "^29.7.0", 46 | "ts-jest": "^29.2.4", 47 | "tslint": "^6.1.3", 48 | "typedoc": "^0.26.6", 49 | "typescript": "^5.5.4" 50 | }, 51 | "jest": { 52 | "collectCoverageFrom": [ 53 | "src/**/*.ts" 54 | ], 55 | "testEnvironment": "node", 56 | "transform": { 57 | "^.+\\.ts$": "ts-jest" 58 | }, 59 | "testRegex": "tests/.*\\.test\\.ts$", 60 | "moduleFileExtensions": [ 61 | "ts", 62 | "js" 63 | ], 64 | "testPathIgnorePatterns": [ 65 | "/node_modules/" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/definition/GraphQLTypeIndex.ts: -------------------------------------------------------------------------------- 1 | import { Definition, DefinitionConfig } from '../metadata/Definition'; 2 | import { GraphQLNamedType } from 'graphql'; 3 | import { isOutputType, isInputType } from 'graphql/type/definition'; 4 | 5 | 6 | export interface GraphQLTypeIndexConfig extends DefinitionConfig { 7 | typeInstance: GraphQLNamedType; 8 | } 9 | 10 | /** 11 | * Metadata for GraphQLType object. 12 | */ 13 | export class GraphQLTypeIndex extends Definition { 14 | constructor(config: { 15 | definitionClass: Function | null; 16 | typeInstance: GraphQLNamedType; 17 | }) { 18 | const configs: GraphQLTypeIndexConfig = { 19 | definitionClass: config.definitionClass, 20 | definitionName: config.typeInstance.name, 21 | typeInstance: config.typeInstance, 22 | }; 23 | super(configs); 24 | } 25 | 26 | public get definitionName() { return this.config.definitionName; } 27 | public isOutputType() { return isOutputType(this.config.typeInstance); } 28 | public isInputType() { return isInputType(this.config.typeInstance); } 29 | 30 | public buildTypeInstance() { 31 | return this.config.typeInstance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/definition/InputType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInputFieldConfig, GraphQLInputFieldConfigMap, GraphQLInputObjectType, GraphQLInputType } from 'graphql'; 2 | 3 | import { Definition, DefinitionConfig, DefinitionKind } from '../metadata/Definition'; 4 | import { InputField } from '../reference/InputField'; 5 | import { TypeResolvingContext } from '../type-expression/types'; 6 | import { defaultInputFieldInstantiator } from '../types'; 7 | 8 | 9 | export interface InputTypeConfig extends DefinitionConfig {} 10 | 11 | /** 12 | * Metadata type for InputObjectType 13 | */ 14 | export class InputType extends Definition { 15 | public get kind(): DefinitionKind { return 'input'; } 16 | 17 | public buildTypeInstance(context: TypeResolvingContext) { 18 | const name = this.definitionName; 19 | const description = this.description; 20 | const fields = this.buildInputFieldConfigMap.bind(this, context); 21 | return new GraphQLInputObjectType({ name, fields, description }); 22 | } 23 | 24 | protected buildInputFieldConfigMap(context: TypeResolvingContext): GraphQLInputFieldConfigMap { 25 | const fields = this.findReference(context, InputField); 26 | 27 | return fields.reduce((results, field) => { 28 | results[field.fieldName] = this.buildInputFieldConfig(context, field); 29 | return results; 30 | }, {} as GraphQLInputFieldConfigMap); 31 | } 32 | 33 | protected buildInputFieldConfig(context: TypeResolvingContext, field: InputField): GraphQLInputFieldConfig { 34 | return { 35 | type: field.resolveType(context.storage) as GraphQLInputType, 36 | defaultValue: field.defaultValue, 37 | description: field.description, 38 | }; 39 | } 40 | 41 | protected instantiationCache = new WeakMap(); 42 | 43 | public buildInstantiator(context: TypeResolvingContext) { 44 | if (!this.definitionClass) { 45 | return defaultInputFieldInstantiator; 46 | } 47 | 48 | const fields = this.findReference(context, InputField); 49 | 50 | const fieldInstantiators = fields.reduce((res, field) => { 51 | res[field.fieldName] = field.buildInstantiator(context.storage); 52 | return res; 53 | }, {} as any); 54 | 55 | const instantiator = (values: any) => { 56 | let cached = this.instantiationCache.get(values); 57 | if (!cached) { 58 | cached = new (this.definitionClass as any)(); 59 | Object.keys(values).forEach(fieldName => { 60 | cached[fieldName] = fieldInstantiators[fieldName](values[fieldName]); 61 | }); 62 | this.instantiationCache.set(values, cached); 63 | } 64 | return this.instantiationCache.get(values); 65 | }; 66 | 67 | Object.defineProperty(instantiator, 'name', { value: 'instantiate' + this.definitionName }); 68 | return instantiator; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/definition/InterfaceType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfig, GraphQLFieldConfigMap, GraphQLInterfaceType, GraphQLTypeResolver, GraphQLOutputType } from 'graphql'; 2 | 3 | import { Definition, DefinitionConfig, DefinitionKind } from '../metadata/Definition'; 4 | import { Field } from '../reference/Field'; 5 | import { TypeResolvingContext } from '../type-expression/types'; 6 | 7 | 8 | export interface InterfaceTypeConfig extends DefinitionConfig { 9 | resolveType?: GraphQLTypeResolver; 10 | description?: string; 11 | } 12 | 13 | /** 14 | * Metadata type for InterfaceType 15 | */ 16 | export class InterfaceType extends Definition { 17 | public get kind(): DefinitionKind { return 'output'; } 18 | 19 | protected buildFieldConfig(context: TypeResolvingContext, field: Field): GraphQLFieldConfig { 20 | const { description, deprecationReason } = field; 21 | 22 | return { 23 | type: field.resolveType(context.storage) as GraphQLOutputType, 24 | args: field.buildArgumentMap(context), 25 | resolve: field.buildResolver(context), 26 | description, 27 | deprecationReason, 28 | }; 29 | } 30 | 31 | protected buildFieldConfigMap(context: TypeResolvingContext): GraphQLFieldConfigMap { 32 | const fields = this.findReference(context, Field); 33 | return ( 34 | fields.reduce((results, field) => { 35 | const name = field.fieldName; 36 | 37 | results[name] = this.buildFieldConfig(context, field); 38 | return results; 39 | }, {} as GraphQLFieldConfigMap) 40 | ); 41 | } 42 | 43 | public buildTypeInstance(context: TypeResolvingContext): GraphQLInterfaceType { 44 | const name = this.definitionName; 45 | const description = this.description; 46 | const fields = this.buildFieldConfigMap.bind(this, context); 47 | 48 | return new GraphQLInterfaceType({ name, fields, description }); 49 | } 50 | } -------------------------------------------------------------------------------- /src/definition/ObjectType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfig, GraphQLFieldConfigMap, GraphQLInterfaceType, GraphQLObjectType, GraphQLOutputType } from 'graphql'; 2 | 3 | import { Definition, DefinitionConfig, DefinitionKind } from '../metadata/Definition'; 4 | import { Field } from '../reference/Field'; 5 | import { Implement } from '../reference/Implement'; 6 | import { TypeResolvingContext } from '../type-expression/types'; 7 | 8 | 9 | export interface ObjectTypeConfig extends DefinitionConfig { 10 | description?: string; 11 | } 12 | 13 | /** 14 | * Metadata type for ObjectType 15 | */ 16 | export class ObjectType extends Definition { 17 | public get kind(): DefinitionKind { return 'output'; } 18 | 19 | /** 20 | * Build GraphQLObjectType instance from metadata. 21 | */ 22 | public buildTypeInstance(context: TypeResolvingContext): GraphQLObjectType { 23 | const name = this.definitionName; 24 | const description = this.description; 25 | const fields = this.buildFieldConfigMap.bind(this, context); 26 | const interfaces = this.findInterfaces(context); 27 | const isTypeOf = this.buildIsTypeOf(context); 28 | return new GraphQLObjectType({ name, fields, interfaces, description, isTypeOf }); 29 | } 30 | 31 | protected buildFieldConfigMap(context: TypeResolvingContext): GraphQLFieldConfigMap { 32 | 33 | const fields = this.findReference(context, Field); 34 | return ( 35 | fields.reduce((results, field) => { 36 | const name = field.fieldName; 37 | results[name] = this.buildFieldConfig(context, field); 38 | return results; 39 | }, {} as GraphQLFieldConfigMap) 40 | ); 41 | } 42 | 43 | protected buildFieldConfig(context: TypeResolvingContext, field: Field): GraphQLFieldConfig { 44 | const { description, deprecationReason } = field; 45 | 46 | return { 47 | type: field.resolveType(context.storage) as GraphQLOutputType, 48 | args: field.buildArgumentMap(context), 49 | resolve: field.buildResolver(context), 50 | description, 51 | deprecationReason, 52 | }; 53 | } 54 | 55 | protected findInterfaces(context: TypeResolvingContext): GraphQLInterfaceType[] { 56 | const impls = this.findReference(context, Implement); 57 | return impls.map(impl => impl.resolveType(context.storage) as GraphQLInterfaceType); 58 | } 59 | 60 | protected buildIsTypeOf(context: TypeResolvingContext) { 61 | const { definitionClass } = this; 62 | if (definitionClass instanceof Function) { 63 | return (source: any) => (source instanceof definitionClass); 64 | } else { 65 | return () => true; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/definition/SubscriptionType.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType } from './ObjectType'; 2 | import { Field } from '../reference/Field'; 3 | import { TypeResolvingContext } from '../type-expression/types'; 4 | 5 | 6 | export class SubscriptionType extends ObjectType { 7 | protected buildFieldConfig(context: TypeResolvingContext, field: Field) { 8 | const { resolve, ...rest } = super.buildFieldConfig(context, field); 9 | // it constrains the given async iterator directly returns an object with the expected type 10 | return { ...rest, subscribe: resolve, resolve: (v: any) => v }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/definition/rootTypes.ts: -------------------------------------------------------------------------------- 1 | import { MetadataStorage } from '../metadata/MetadataStorage'; 2 | import { ObjectType } from './ObjectType'; 3 | import { SubscriptionType } from './SubscriptionType'; 4 | 5 | 6 | /** 7 | * Fallback class for Query type. 8 | * 9 | * It can be overridden by defining custom Query class with [defineType] decorator 10 | */ 11 | export class Query {} 12 | 13 | /** 14 | * Fallback class for Mutation type. 15 | * 16 | * It can be overridden by defining custom Mutation class with [defineType] decorator 17 | */ 18 | export class Mutation {} 19 | 20 | /** 21 | * Fallback class for Subscription type. 22 | * 23 | * It can be overridden by defining custom Mutation class with [defineType] decorator 24 | */ 25 | export class Subscription {} 26 | 27 | export function loadFallbackRootTypes(storage: MetadataStorage) { 28 | storage.registerMetadata([ 29 | new ObjectType({ 30 | definitionClass: Query, 31 | definitionName: 'Query' 32 | }), 33 | new ObjectType({ 34 | definitionClass: Mutation, 35 | definitionName: 'Mutation' 36 | }), 37 | new SubscriptionType({ 38 | definitionClass: Subscription, 39 | definitionName: 'Subscription', 40 | }) 41 | ]); 42 | } 43 | -------------------------------------------------------------------------------- /src/definition/scalar.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLString, GraphQLBoolean, GraphQLFloat, GraphQLInt, GraphQLID } from 'graphql'; 2 | import { MetadataStorage } from '../metadata/MetadataStorage'; 3 | import { GraphQLTypeIndex } from './GraphQLTypeIndex'; 4 | 5 | /** 6 | * Load all the built in scalar types into a given [[MetadataStorage]]. 7 | * @param storage 8 | */ 9 | export function loadBuiltInScalar(storage: MetadataStorage) { 10 | storage.registerMetadata([ 11 | new GraphQLTypeIndex({ 12 | definitionClass: null, 13 | typeInstance: GraphQLString, 14 | }), 15 | new GraphQLTypeIndex({ 16 | definitionClass: null, 17 | typeInstance: GraphQLBoolean, 18 | }), 19 | new GraphQLTypeIndex({ 20 | definitionClass: null, 21 | typeInstance: GraphQLFloat, 22 | }), 23 | new GraphQLTypeIndex({ 24 | definitionClass: null, 25 | typeInstance: GraphQLInt, 26 | }), 27 | new GraphQLTypeIndex({ 28 | definitionClass: null, 29 | typeInstance: GraphQLID, 30 | }), 31 | ]); 32 | } 33 | -------------------------------------------------------------------------------- /src/global.ts: -------------------------------------------------------------------------------- 1 | import { loadFallbackRootTypes } from './definition/rootTypes'; 2 | import { loadBuiltInScalar } from './definition/scalar'; 3 | import { DefinitionKind } from './metadata/Definition'; 4 | import { MetadataStorage } from './metadata/MetadataStorage'; 5 | import { DefinitionParser } from './sdl/ast'; 6 | import { coerceType } from './type-expression/coerceType'; 7 | import { TypeExpression } from './type-expression/TypeExpression'; 8 | import { Thunk, ResolverMap } from './types'; 9 | import { TypeArg } from './type-expression/types'; 10 | 11 | 12 | /** 13 | * Create a new MetadataStorage initialized with default metadata 14 | */ 15 | export function createMetadataStorage() { 16 | const storage = new MetadataStorage(); 17 | loadBuiltInScalar(storage); 18 | loadFallbackRootTypes(storage); 19 | return storage; 20 | } 21 | 22 | /** 23 | * Global MetadataStorage used by default. 24 | */ 25 | export let globalMetadataStorage: MetadataStorage; 26 | 27 | export function getGlobalMetadataStorage() { 28 | if (!globalMetadataStorage) { 29 | globalMetadataStorage = createMetadataStorage(); 30 | } 31 | return globalMetadataStorage; 32 | } 33 | 34 | /** 35 | * Get a GraphQLType instance from the given storage or default 36 | * global metadata storage. 37 | * @param typeArg 38 | * @param storage 39 | */ 40 | export function getType(typeArg: TypeExpression | TypeArg, kind: DefinitionKind = 'any'): any { 41 | const storage = getGlobalMetadataStorage(); 42 | return coerceType(typeArg).getType({ kind, storage }); 43 | } 44 | 45 | /** 46 | * Define a type linked to decorated class and add it to the given 47 | * storage or default global metadata storage. 48 | * @param metadataOrThunk 49 | */ 50 | export function defineType(parsersOrThunk: DefinitionParser[] | Thunk) { 51 | const storage = getGlobalMetadataStorage(); 52 | 53 | return function defDecoratorFn(definitionClass: T) { 54 | 55 | storage.deferRegister(() => { 56 | (Array.isArray(parsersOrThunk) ? parsersOrThunk : parsersOrThunk()).forEach(parser => { 57 | storage.registerMetadata(parser.parseWithResolverMap(definitionClass)); 58 | }); 59 | }); 60 | 61 | return definitionClass; 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { gql } from './sdl/gql'; 2 | 3 | export * from './definition/GraphQLTypeIndex'; 4 | export * from './definition/InputType'; 5 | export * from './definition/InterfaceType'; 6 | export * from './definition/ObjectType'; 7 | export * from './definition/rootTypes'; 8 | export * from './definition/scalar'; 9 | export * from './definition/SubscriptionType'; 10 | 11 | 12 | export * from './reference/Field'; 13 | export * from './reference/Implement'; 14 | export * from './reference/InputField'; 15 | export * from './global'; 16 | 17 | export * from './type-expression/DefinitionTypeExpression'; 18 | export * from './type-expression/GraphQLTypeExpression'; 19 | export * from './type-expression/TypeExpression'; 20 | export * from './type-expression/structure'; 21 | export * from './type-expression/coerceType'; 22 | 23 | export * from './metadata/Definition'; 24 | export * from './metadata/MetadataStorage'; 25 | export * from './metadata/Reference'; 26 | 27 | export * from './relay/connection'; 28 | export * from './relay/mutation'; 29 | export * from './relay/node'; 30 | -------------------------------------------------------------------------------- /src/metadata/Definition.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNamedType } from 'graphql'; 2 | import { defaultInputFieldInstantiator, Instantiator } from '../types'; 3 | import { TypeResolvingContext } from '../type-expression/types'; 4 | import { Reference } from './Reference'; 5 | 6 | 7 | export type DefinitionKind = 'any' | 'input' | 'output'; 8 | 9 | export interface DefinitionConfig { 10 | definitionClass: Object | null; 11 | definitionName: string; 12 | description?: string; 13 | directives?: any; 14 | } 15 | 16 | /** 17 | * Contain configs required to build named GraphQL types. 18 | * Guarantee its type instance only created once. 19 | */ 20 | export class Definition { 21 | 22 | public readonly config: TConfig; 23 | public get kind(): DefinitionKind { return 'any'; } 24 | public get definitionClass() { return this.config.definitionClass; } 25 | public get definitionName() { return this.config.definitionName; } 26 | public get description(): string | undefined { return this.config.description; } 27 | 28 | public constructor(config: TConfig) { 29 | this.config = config; 30 | } 31 | 32 | protected graphqlType: GraphQLNamedType; 33 | 34 | public getOrCreateTypeInstance(context: TypeResolvingContext): GraphQLNamedType { 35 | if (!this.graphqlType) { 36 | this.graphqlType = this.buildTypeInstance(context); 37 | } 38 | return this.graphqlType; 39 | } 40 | 41 | public findReference(context: TypeResolvingContext, referenceClass: { new(v: any): T }) { 42 | const referenceByName = context.storage.findReference(referenceClass, this.definitionName); 43 | const referenceByClass = this.definitionClass 44 | ? context.storage.findReference(referenceClass, this.definitionClass) 45 | : []; 46 | return [...referenceByName, ...referenceByClass]; 47 | } 48 | 49 | /** 50 | * Build GraphQLType instance from metadata. 51 | */ 52 | public buildTypeInstance(context: TypeResolvingContext): GraphQLNamedType { 53 | throw new Error(`Should implement typeInstance getter in ${this.constructor.name}`); 54 | } 55 | 56 | public buildInstantiator(context: TypeResolvingContext): Instantiator { 57 | return defaultInputFieldInstantiator; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/metadata/MetadataStorage.ts: -------------------------------------------------------------------------------- 1 | import { Definition, DefinitionKind } from './Definition'; 2 | import { Reference } from './Reference'; 3 | 4 | 5 | export type Metadata = Reference | Definition; 6 | 7 | /** 8 | * Keep all [[Definition]] and references. 9 | * Provide methods to query metadata with its associated class or graphql type name. 10 | */ 11 | export class MetadataStorage { 12 | protected readonly deferredRegister: Array<() => void> = []; 13 | protected deferredResolved = false; 14 | 15 | public readonly definitions: Definition[] = []; 16 | public readonly references: Reference[] = []; 17 | 18 | public deferRegister(metadataFn: () => void) { 19 | this.deferredRegister.unshift(metadataFn); 20 | } 21 | 22 | protected resolveDeferred() { 23 | while (this.deferredRegister.length > 0) { 24 | this.deferredRegister.pop()!(); 25 | } 26 | } 27 | 28 | public registerMetadata(metadata: Metadata[]) { 29 | metadata.map(entry => { 30 | if (entry instanceof Definition) { 31 | this.definitions.push(entry); 32 | } else if (entry instanceof Reference) { 33 | this.references.push(entry); 34 | } 35 | }); 36 | } 37 | 38 | /** 39 | * 40 | * Get a [[Definition]] object which is instance of the `metadataClass` and associated to `linkedClass` 41 | * @param metadataClass A [[Definition]] subclass to query 42 | * @param definitionKey A class associated with metadata to query 43 | * @param asKind 44 | */ 45 | public getDefinition(metadataClass: { new (...args: any[]): T; }, definitionKey: Object, asKind: DefinitionKind): T | undefined { 46 | this.resolveDeferred(); 47 | 48 | const entry = this.definitions.find(metadata => { 49 | let classOrNameMatched: boolean; 50 | if (typeof definitionKey === 'string') { 51 | classOrNameMatched = metadata.definitionName === definitionKey; 52 | } else { 53 | classOrNameMatched = metadata.definitionClass === definitionKey; 54 | } 55 | const typeMatched = asKind === 'any' || metadata.kind === 'any' || metadata.kind === asKind; 56 | return classOrNameMatched && (metadata instanceof metadataClass) && typeMatched; 57 | }); 58 | 59 | return entry as T; 60 | } 61 | 62 | public findReference(metadataClass: { new (...args: any[]): T }, definitionKey: Object): T[] { 63 | this.resolveDeferred(); 64 | 65 | const entries = this.references.filter(metadata => { 66 | let classOrNameMatched: boolean; 67 | if (definitionKey instanceof Function) { 68 | classOrNameMatched = metadata.source instanceof Function && equalsOrInherits(definitionKey, metadata.source); 69 | } 70 | else { 71 | classOrNameMatched = metadata.source === definitionKey; 72 | } 73 | // const typeMatched = metadata.kind === asKind; 74 | return classOrNameMatched && (metadata instanceof metadataClass); 75 | }); 76 | return entries as T[]; 77 | } 78 | } 79 | 80 | export function equalsOrInherits(cls: Function, superClass: Function) { 81 | return cls === superClass || cls.prototype instanceof superClass; 82 | } 83 | -------------------------------------------------------------------------------- /src/metadata/Reference.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLType } from 'graphql'; 2 | 3 | import { TypeExpression } from '../type-expression/TypeExpression'; 4 | import { Instantiator } from '../types'; 5 | import { MetadataStorage } from './MetadataStorage'; 6 | 7 | 8 | export interface ReferenceConfig { 9 | source: Object; 10 | target: TypeExpression; 11 | } 12 | 13 | export type ReferenceKind = 'input' | 'output'; 14 | 15 | export abstract class Reference { 16 | protected abstract kind: ReferenceKind; 17 | public constructor(protected config: TConfig) { } 18 | 19 | public get source() { return this.config.source; } 20 | public get target() { return this.config.target; } 21 | // public get extendingTypeName() { return this.config.extendingTypeName; } 22 | 23 | public resolveType(storage: MetadataStorage): GraphQLType { 24 | return this.target.getType({ storage, kind: this.kind }); 25 | } 26 | 27 | public buildInstantiator(storage: MetadataStorage): Instantiator { 28 | return this.target.getInstantiator({ storage, kind: this.kind }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/reference/Field.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfigArgumentMap, GraphQLInputType, GraphQLFieldResolver } from 'graphql'; 2 | 3 | import { InputField } from './InputField'; 4 | import { Reference, ReferenceConfig } from '../metadata/Reference'; 5 | import { TypeResolvingContext } from '../type-expression/types'; 6 | 7 | 8 | export interface FieldConfig extends ReferenceConfig { 9 | fieldName: string; 10 | args: InputField[]; 11 | description?: string; 12 | deprecationReason?: string; 13 | directives?: any; 14 | resolver: GraphQLFieldResolver; 15 | } 16 | 17 | export class Field extends Reference { 18 | protected get kind(): 'output' { return 'output'; } 19 | 20 | public get fieldName() { return this.config.fieldName; } 21 | public get description() { return this.config.description; } 22 | public get deprecationReason() { return this.config.deprecationReason; } 23 | public get resolver() { return this.config.resolver; } 24 | 25 | public buildArgumentMap(context: TypeResolvingContext): GraphQLFieldConfigArgumentMap { 26 | const { args } = this.config; 27 | return args.reduce((args, ref) => { 28 | args[ref.fieldName] = { 29 | type: ref.resolveType(context.storage) as GraphQLInputType, 30 | defaultValue: ref.defaultValue, 31 | description: ref.description, 32 | }; 33 | return args; 34 | }, {} as GraphQLFieldConfigArgumentMap); 35 | } 36 | 37 | public buildResolver(context: TypeResolvingContext): GraphQLFieldResolver { 38 | const { args, resolver } = this.config; 39 | 40 | const instantiators = args.reduce((res, meta) => { 41 | res[meta.fieldName] = meta.buildInstantiator(context.storage); 42 | return res; 43 | }, {} as any); 44 | 45 | const argumentInstantiator = (argValues: any) => { 46 | return Object.keys(argValues).reduce((res, fieldName) => { 47 | res[fieldName] = instantiators[fieldName](argValues[fieldName]); 48 | return res; 49 | }, {} as any); 50 | }; 51 | 52 | const finalResolver = function(source: any, args: any, context: any, info: any) { 53 | return resolver(source, argumentInstantiator(args), context, info); 54 | }; 55 | return finalResolver; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/reference/Implement.ts: -------------------------------------------------------------------------------- 1 | import { Reference, ReferenceConfig } from '../metadata/Reference'; 2 | 3 | 4 | export interface ImplementConfig extends ReferenceConfig {} 5 | 6 | export class Implement extends Reference { 7 | protected get kind(): 'output' { return 'output'; } 8 | } 9 | -------------------------------------------------------------------------------- /src/reference/InputField.ts: -------------------------------------------------------------------------------- 1 | import { Reference, ReferenceConfig } from '../metadata/Reference'; 2 | 3 | 4 | export interface InputFieldConfig extends ReferenceConfig { 5 | fieldName: string; 6 | defaultValue?: any; 7 | description?: string; 8 | directives?: any; 9 | } 10 | 11 | export class InputField extends Reference { 12 | protected get kind(): 'input' { return 'input'; } 13 | 14 | public get fieldName() { return this.config.fieldName; } 15 | public get defaultValue() { return this.config.defaultValue; } 16 | public get description() { return this.config.description; } 17 | public get directives() { return this.config.directives; } 18 | } 19 | -------------------------------------------------------------------------------- /src/relay/connection.ts: -------------------------------------------------------------------------------- 1 | import { defaultFieldResolver, GraphQLBoolean, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'; 2 | 3 | import { ObjectType } from '../definition/ObjectType'; 4 | import { getGlobalMetadataStorage } from '../global'; 5 | import { Field } from '../reference/Field'; 6 | import { bindStaticResolver } from '../sdl/ast'; 7 | import { coerceType } from '../type-expression/coerceType'; 8 | import { List, NonNull } from '../type-expression/structure'; 9 | import { TypeExpression } from '../type-expression/TypeExpression'; 10 | import { TypeArg } from '../type-expression/types'; 11 | import { ResolverMap } from '../types'; 12 | 13 | 14 | type ConnectionByNode = { node: TypeArg | TypeExpression, edge?: undefined }; 15 | type ConnectionByEdge = { edge: TypeArg | TypeExpression, node?: undefined }; 16 | type DefineConnectionOptions = { typeName?: string } & (ConnectionByNode | ConnectionByEdge); 17 | 18 | 19 | export function defineConnection(options: DefineConnectionOptions) { 20 | return function registerConnectionMetadata(definitionClass: ResolverMap) { 21 | const storage = getGlobalMetadataStorage(); 22 | 23 | storage.deferRegister(() => { 24 | const connectionName = options.typeName || (definitionClass as any).name; 25 | if (!connectionName) { 26 | throw new Error(`Should provide connection typeName`); 27 | } 28 | 29 | let edge: TypeExpression; 30 | const node = coerceType(options.node!); 31 | const nodeTypeName = node.getTypeName({ storage, kind: 'output' }); 32 | 33 | if (options.edge) { 34 | edge = coerceType(options.edge); 35 | } else { 36 | const edgeTypeName = `${nodeTypeName}Edge`; 37 | defineEdge({ typeName: edgeTypeName, node })(null); 38 | edge = coerceType(edgeTypeName); 39 | } 40 | 41 | storage.registerMetadata([ 42 | new ObjectType({ 43 | definitionClass, 44 | definitionName: connectionName, 45 | description: 'A connection to a list of items.' 46 | }), 47 | new Field({ 48 | source: definitionClass, 49 | target: NonNull.of(coerceType(pageInfoType)), 50 | fieldName: 'pageInfo', 51 | description: 'Information to aid in pagination.', 52 | args: [], 53 | resolver: bindStaticResolver(definitionClass, 'pageInfo') || defaultFieldResolver, 54 | }), 55 | new Field({ 56 | source: definitionClass, 57 | target: NonNull.of(List.of(NonNull.of(edge))), 58 | fieldName: 'edges', 59 | description: 'A list of edges.', 60 | args: [], 61 | resolver: bindStaticResolver(definitionClass, 'edges') || defaultFieldResolver, 62 | }), 63 | ]); 64 | }); 65 | }; 66 | } 67 | 68 | type DefineEdgeOptions = { typeName?: string, node: TypeArg | TypeExpression }; 69 | 70 | export function defineEdge(options: DefineEdgeOptions) { 71 | return function registerEdgeMetadata(definitionClass: ResolverMap | null) { 72 | const storage = getGlobalMetadataStorage(); 73 | storage.deferRegister(() => { 74 | const node = coerceType(options.node); 75 | const nodeTypeName = node.getTypeName({ storage, kind: 'output' }); 76 | 77 | const edgeName = options.typeName 78 | || (definitionClass && (definitionClass as any).name) 79 | || `${nodeTypeName}Edge`; 80 | 81 | storage.registerMetadata([ 82 | new ObjectType({ 83 | definitionClass, 84 | definitionName: edgeName, 85 | description: 'An edge in a connection.', 86 | }), 87 | new Field({ 88 | source: definitionClass || edgeName, 89 | target: NonNull.of(node), 90 | fieldName: 'node', 91 | description: 'The item at the end of the edge', 92 | args: [], 93 | resolver: definitionClass && bindStaticResolver(definitionClass, 'pageInfo') || defaultFieldResolver, 94 | }), 95 | new Field({ 96 | source: definitionClass || edgeName, 97 | target: NonNull.of(coerceType('String')), 98 | fieldName: 'cursor', 99 | description: 'A cursor for use in pagination', 100 | args: [], 101 | resolver: definitionClass && bindStaticResolver(definitionClass, 'cursor') || defaultFieldResolver, 102 | }), 103 | ]); 104 | }); 105 | }; 106 | } 107 | 108 | const pageInfoType = new GraphQLObjectType({ 109 | name: 'PageInfo', 110 | description: 'Information about pagination in a connection.', 111 | fields: () => ({ 112 | hasNextPage: { 113 | type: new GraphQLNonNull(GraphQLBoolean), 114 | description: 'When paginating forwards, are there more items?' 115 | }, 116 | hasPreviousPage: { 117 | type: new GraphQLNonNull(GraphQLBoolean), 118 | description: 'When paginating backwards, are there more items?' 119 | }, 120 | startCursor: { 121 | type: GraphQLString, 122 | description: 'When paginating backwards, the cursor to continue.' 123 | }, 124 | endCursor: { 125 | type: GraphQLString, 126 | description: 'When paginating forwards, the cursor to continue.' 127 | }, 128 | }) 129 | }); 130 | -------------------------------------------------------------------------------- /src/relay/mutation.ts: -------------------------------------------------------------------------------- 1 | import { defineType } from '../global'; 2 | import { gql } from '../sdl/gql'; 3 | 4 | 5 | @defineType(gql` 6 | type RelayMutationPayload { 7 | clientMutationId: String! 8 | } 9 | input RelayMutationInput { 10 | clientMutationId: String! 11 | } 12 | `) 13 | export abstract class RelayMutation { 14 | clientMutationId: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/relay/node.ts: -------------------------------------------------------------------------------- 1 | import { defineType } from '../global'; 2 | import { gql } from '../sdl/gql'; 3 | 4 | 5 | @defineType(() => gql` 6 | """ 7 | An object with an ID 8 | """ 9 | interface Node { 10 | """ 11 | The id of the object. 12 | """ 13 | id: ID! 14 | } 15 | `) 16 | export abstract class Node {} 17 | -------------------------------------------------------------------------------- /src/sdl/ast.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DefinitionNode, 3 | FieldDefinitionNode, 4 | InputObjectTypeDefinitionNode, 5 | InputValueDefinitionNode, 6 | InterfaceTypeDefinitionNode, 7 | ListTypeNode, 8 | NamedTypeNode, 9 | NonNullTypeNode, 10 | ObjectTypeDefinitionNode, 11 | ObjectTypeExtensionNode, 12 | InterfaceTypeExtensionNode, 13 | InputObjectTypeExtensionNode, 14 | DirectiveNode, 15 | ArgumentNode, 16 | ObjectFieldNode, 17 | ValueNode, 18 | defaultFieldResolver, 19 | } from 'graphql'; 20 | 21 | import { Metadata } from '../metadata/MetadataStorage'; 22 | import { GraphQLFieldResolver } from 'graphql/type/definition'; 23 | import { SubscriptionType } from '../definition/SubscriptionType'; 24 | import { ObjectType } from '../definition/ObjectType'; 25 | import { InterfaceType } from '../definition/InterfaceType'; 26 | import { InputType } from '../definition/InputType'; 27 | import { TypeExpression } from '../type-expression/TypeExpression'; 28 | import { coerceType } from '../type-expression/coerceType'; 29 | import { List, NonNull } from '../type-expression/structure'; 30 | import { Field } from '../reference/Field'; 31 | import { InputField } from '../reference/InputField'; 32 | import { Implement } from '../reference/Implement'; 33 | import { TypeArg } from '../type-expression/types'; 34 | import { ResolverMap } from '../types'; 35 | 36 | 37 | export interface SubstitutionMap { 38 | [tempName: string]: TypeExpression | TypeArg; 39 | } 40 | 41 | export class DefinitionParser { 42 | 43 | private metadata: Metadata[]; 44 | 45 | constructor( 46 | protected rootNode: DefinitionNode, 47 | protected subsMap: SubstitutionMap, 48 | ) {} 49 | 50 | public parseWithResolverMap(definitionClass: ResolverMap): Metadata[] { 51 | const { rootNode } = this; 52 | this.metadata = []; 53 | 54 | if (rootNode.kind === 'ObjectTypeDefinition') { 55 | this.handleObjectTypeDefinition(rootNode, definitionClass); 56 | } 57 | else if (rootNode.kind === 'ObjectTypeExtension') { 58 | this.handleObjectTypeExtension(rootNode, definitionClass); 59 | } 60 | else if (rootNode.kind === 'InterfaceTypeDefinition') { 61 | this.handleInterfaceTypeDefinition(rootNode, definitionClass); 62 | } 63 | else if (rootNode.kind === 'InterfaceTypeExtension') { 64 | this.handleInterfaceTypeExtension(rootNode, definitionClass); 65 | } 66 | else if (rootNode.kind === 'InputObjectTypeDefinition') { 67 | this.handleInputObjectTypeDefinition(rootNode, definitionClass); 68 | } 69 | else if (rootNode.kind === 'InputObjectTypeExtension') { 70 | this.handleInputObjectExtension(rootNode, definitionClass); 71 | } 72 | else { 73 | throw new Error(`Node type not supported: ${rootNode.kind}`); 74 | } 75 | return this.metadata; 76 | } 77 | 78 | protected completeTypeExpression( 79 | typeNode: NamedTypeNode | ListTypeNode | NonNullTypeNode, 80 | ): TypeExpression { 81 | const { subsMap } = this; 82 | 83 | if (typeNode.kind === 'ListType') { 84 | return new List(this.completeTypeExpression(typeNode.type)); 85 | } else if (typeNode.kind === 'NonNullType') { 86 | return new NonNull(this.completeTypeExpression(typeNode.type)); 87 | } 88 | const subType = subsMap[typeNode.name.value]; 89 | return coerceType(subType || typeNode.name.value); 90 | } 91 | 92 | protected handleObjectTypeDefinition(rootNode: ObjectTypeDefinitionNode, definitionClass: ResolverMap): void { 93 | const { name, interfaces, fields, description } = rootNode; 94 | 95 | if (fields) { 96 | fields.forEach(fieldNode => this.appendFieldMetadataConfig(fieldNode, definitionClass)); 97 | } 98 | if (interfaces) { 99 | interfaces.forEach(interfaceNode => this.appendImplementTypeExpression(interfaceNode, definitionClass)); 100 | } 101 | 102 | let metadata; 103 | if (name.value === 'Subscription') { 104 | metadata = new SubscriptionType({ 105 | definitionClass, 106 | definitionName: name.value, 107 | description: description && description.value, 108 | directives: rootNode.directives && this.completeDirectives(rootNode.directives), 109 | }); 110 | } else { 111 | metadata = new ObjectType({ 112 | definitionClass, 113 | definitionName: name.value, 114 | description: description && description.value, 115 | directives: rootNode.directives && this.completeDirectives(rootNode.directives), 116 | }); 117 | } 118 | 119 | this.metadata.push(metadata); 120 | } 121 | 122 | protected handleObjectTypeExtension(rootNode: ObjectTypeExtensionNode, definitionClass: ResolverMap): void { 123 | const { name, interfaces, fields } = rootNode; 124 | 125 | if (fields) { 126 | fields.forEach(fieldNode => this.appendFieldMetadataConfig(fieldNode, definitionClass, name.value)); 127 | } 128 | if (interfaces) { 129 | interfaces.forEach(interfaceNode => this.appendImplementTypeExpression(interfaceNode, definitionClass, name.value)); 130 | } 131 | } 132 | 133 | protected handleInterfaceTypeDefinition(rootNode: InterfaceTypeDefinitionNode, definitionClass: ResolverMap): void { 134 | const { name, description, fields } = rootNode; 135 | 136 | if (fields) { 137 | fields.forEach(fieldNode => this.appendFieldMetadataConfig(fieldNode, definitionClass)); 138 | } 139 | 140 | this.metadata.push(new InterfaceType({ 141 | definitionClass, 142 | definitionName: name.value, 143 | description: description && description.value, 144 | directives: rootNode.directives && this.completeDirectives(rootNode.directives), 145 | })); 146 | } 147 | 148 | protected handleInterfaceTypeExtension(rootNode: InterfaceTypeExtensionNode, definitionClass: ResolverMap): void { 149 | const { name, fields } = rootNode; 150 | 151 | if (fields) { 152 | fields.forEach(fieldNode => this.appendFieldMetadataConfig(fieldNode, definitionClass, name.value)); 153 | } 154 | } 155 | 156 | protected handleInputObjectTypeDefinition(node: InputObjectTypeDefinitionNode, definitionClass: ResolverMap): void { 157 | const { name, description, fields } = node; 158 | 159 | if (fields) { 160 | fields.forEach(fieldNode => this.appendInputFieldMetadataConfig(fieldNode, definitionClass)); 161 | } 162 | 163 | this.metadata.push(new InputType({ 164 | definitionClass, 165 | definitionName: name.value, 166 | description: description && description.value, 167 | directives: node.directives && this.completeDirectives(node.directives), 168 | })); 169 | } 170 | 171 | protected handleInputObjectExtension(node: InputObjectTypeExtensionNode, definitionClass: ResolverMap): void { 172 | const { name, fields } = node; 173 | 174 | if (fields) { 175 | fields.forEach(fieldNode => this.appendInputFieldMetadataConfig(fieldNode, definitionClass, name.value)); 176 | } 177 | } 178 | 179 | protected appendFieldMetadataConfig(node: FieldDefinitionNode, definitionClass: ResolverMap, extendingTypeName?: string): void { 180 | const { name, type, description, directives, arguments: args } = node; 181 | 182 | const argumentRefs = args && args.map(argumentNode => ( 183 | this.createInputField(argumentNode, definitionClass) 184 | )); 185 | 186 | const resolver = bindStaticResolver(definitionClass, name.value) || defaultFieldResolver; 187 | 188 | this.metadata.push(new Field({ 189 | source: extendingTypeName || definitionClass, 190 | target: this.completeTypeExpression(type), 191 | fieldName: name.value, 192 | args: argumentRefs || [], 193 | description: description && description.value, 194 | directives: directives && this.completeDirectives(directives), 195 | extendingTypeName, 196 | resolver, 197 | })); 198 | } 199 | 200 | protected createInputField(node: InputValueDefinitionNode, definitionClass: ResolverMap, extendingTypeName?: string): InputField { 201 | const { name, type, description, defaultValue, directives } = node; 202 | 203 | return new InputField({ 204 | source: extendingTypeName || definitionClass, 205 | target: this.completeTypeExpression(type), 206 | fieldName: name.value, 207 | directives: directives && this.completeDirectives(directives), 208 | defaultValue: defaultValue && this.completeValueNode(defaultValue), 209 | description: description && description.value, 210 | extendingTypeName, 211 | }); 212 | } 213 | 214 | protected appendInputFieldMetadataConfig(node: InputValueDefinitionNode, definitionClass: ResolverMap, extendingTypeName?: string): void { 215 | const field = this.createInputField(node, definitionClass, extendingTypeName); 216 | this.metadata.push(field); 217 | } 218 | 219 | protected appendImplementTypeExpression(node: NamedTypeNode, definitionClass: ResolverMap, extendingTypeName?: string): void { 220 | this.metadata.push(new Implement({ 221 | source: extendingTypeName || definitionClass, 222 | target: this.completeTypeExpression(node), 223 | })); 224 | } 225 | 226 | protected completeDirectives(directiveNodes: ReadonlyArray): DirectiveMap { 227 | return directiveNodes.reduce((results, node) => { 228 | if (node.arguments instanceof Array) { 229 | results[node.name.value] = this.completeArgumentsOrObjectFields(node.arguments); 230 | } else { 231 | results[node.name.value] = {}; 232 | } 233 | return results; 234 | }, {} as {[key: string]: any}); 235 | } 236 | 237 | protected completeArgumentsOrObjectFields(nodes: ReadonlyArray): any { 238 | return nodes.reduce((results, node) => { 239 | results[node.name.value] = this.completeValueNode(node.value); 240 | return results; 241 | }, {} as any); 242 | } 243 | 244 | protected completeValueNode(node: ValueNode): any { 245 | if (node.kind === 'ObjectValue') { 246 | return this.completeArgumentsOrObjectFields(node.fields); 247 | } else if (node.kind === 'NullValue') { 248 | return null; 249 | } else if (node.kind === 'ListValue') { 250 | return node.values.map(this.completeValueNode.bind(this)); 251 | } else if (node.kind === 'Variable') { 252 | throw new Error(`Cannot use variable in schema directives: ${node.name}`); 253 | } else if (node.kind === 'IntValue' || node.kind === 'FloatValue') { 254 | return Number(node.value); 255 | } 256 | return node.value; 257 | } 258 | } 259 | 260 | export type DirectiveMap = { [key: string]: any }; 261 | 262 | export function bindStaticResolver(definitionClass: ResolverMap, fieldName: string): GraphQLFieldResolver | null { 263 | const maybeStaticResolver = (definitionClass as any)[fieldName]; 264 | return maybeStaticResolver instanceof Function 265 | ? maybeStaticResolver.bind(definitionClass) 266 | : null; 267 | } 268 | -------------------------------------------------------------------------------- /src/sdl/gql.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql'; 2 | 3 | import { SubstitutionMap, DefinitionParser } from './ast'; 4 | import { TypeExpression } from '../type-expression/TypeExpression'; 5 | import { TypeArg } from '../type-expression/types'; 6 | 7 | 8 | const SUBSTITUTION_PREFIX = '__GIRIN__SUBS__'; 9 | 10 | export function gql(strings: TemplateStringsArray, ...interpolated: Array): DefinitionParser[] { 11 | const result = [strings[0]]; 12 | const subsMap: SubstitutionMap = {}; 13 | 14 | for (let i = 0; i < interpolated.length; i++) { 15 | const item = interpolated[i]; 16 | let name: string; 17 | if (typeof item === 'string') { 18 | name = item; 19 | } else { 20 | name = `${SUBSTITUTION_PREFIX}${i}`; 21 | subsMap[name] = item; 22 | } 23 | result.push(name); 24 | result.push(strings[i + 1]); 25 | } 26 | 27 | const ast = parse(result.join('')); 28 | 29 | return ast.definitions.map(rootNode => new DefinitionParser(rootNode, subsMap)); 30 | } 31 | -------------------------------------------------------------------------------- /src/type-expression/DefinitionTypeExpression.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLType } from 'graphql'; 2 | 3 | import { defaultInputFieldInstantiator, Instantiator } from '../types'; 4 | import { TypeExpression } from './TypeExpression'; 5 | import { Definition } from '../metadata/Definition'; 6 | import { TypeResolvingContext } from './types'; 7 | 8 | 9 | /** 10 | * Contain an argument which can be resolved to GraphQLType instance. 11 | */ 12 | export class DefinitionTypeExpression extends TypeExpression { 13 | 14 | constructor(protected typeArg: Object | string) { super(); } 15 | 16 | getTypeName(context: TypeResolvingContext): string { 17 | return this.resolveDefinition(context).definitionName; 18 | } 19 | 20 | public getType(context: TypeResolvingContext): GraphQLType { 21 | return this.resolveDefinition(context).getOrCreateTypeInstance(context); 22 | } 23 | 24 | public getInstantiator(context: TypeResolvingContext): Instantiator { 25 | let def; 26 | try { def = this.resolveDefinition(context); } catch(err) {} 27 | return def ? def.buildInstantiator(context) : defaultInputFieldInstantiator; 28 | } 29 | 30 | protected resolveDefinition(context: TypeResolvingContext) { 31 | const def = context.storage.getDefinition(Definition, this.typeArg, context.kind); 32 | if (!def) { 33 | throw new Error(`Cannot resolve type: ${this.typeArg}`); 34 | } 35 | return def; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/type-expression/GraphQLTypeExpression.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLType } from 'graphql'; 2 | 3 | import { defaultInputFieldInstantiator, Instantiator } from '../types'; 4 | import { TypeExpression } from './TypeExpression'; 5 | import { isNamedType } from 'graphql/type/definition'; 6 | 7 | 8 | export class GraphQLTypeExpression extends TypeExpression { 9 | constructor(protected typeInstance: GraphQLType) { 10 | super(); 11 | } 12 | 13 | public getTypeName() { 14 | const { typeInstance } = this; 15 | if (isNamedType(typeInstance)) { 16 | return typeInstance.name; 17 | } else { 18 | throw new Error(`Cannot resolve name: ${typeInstance} is not a GraphQLNamedType`); 19 | } 20 | } 21 | 22 | public getType(): GraphQLType { 23 | return this.typeInstance; 24 | } 25 | public getInstantiator(): Instantiator { 26 | return defaultInputFieldInstantiator; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/type-expression/TypeExpression.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLType } from 'graphql'; 2 | import { Instantiator } from '../types'; 3 | import { TypeResolvingContext } from './types'; 4 | 5 | 6 | export abstract class TypeExpression { 7 | abstract getTypeName(context: TypeResolvingContext): string; 8 | abstract getType(context: TypeResolvingContext): GraphQLType; 9 | abstract getInstantiator(context: TypeResolvingContext): Instantiator; 10 | } 11 | -------------------------------------------------------------------------------- /src/type-expression/coerceType.ts: -------------------------------------------------------------------------------- 1 | import { isType } from 'graphql'; 2 | 3 | import { TypeExpression } from './TypeExpression'; 4 | import { GraphQLTypeExpression } from './GraphQLTypeExpression'; 5 | import { DefinitionTypeExpression } from './DefinitionTypeExpression'; 6 | import { formatObjectInfo } from '../utilities/formatObjectInfo'; 7 | import { TypeArg } from './types'; 8 | 9 | 10 | /** 11 | * Coerce the given argument to a [TypeExpression] 12 | * @param arg 13 | */ 14 | export function coerceType(arg: TypeArg | TypeExpression): TypeExpression { 15 | if (arg instanceof TypeExpression) { 16 | return arg; 17 | } 18 | if (isType(arg)) { 19 | return new GraphQLTypeExpression(arg); 20 | } 21 | if (arg instanceof Function || typeof arg === 'string' || arg instanceof Object) { 22 | return new DefinitionTypeExpression(arg); 23 | } 24 | throw new Error(`Cannot coerce argument to type: ${formatObjectInfo(arg)}`); 25 | } 26 | -------------------------------------------------------------------------------- /src/type-expression/structure.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLType } from 'graphql'; 2 | 3 | import { TypeExpression } from './TypeExpression'; 4 | import { Instantiator } from '../types'; 5 | import { GraphQLList, GraphQLNonNull } from 'graphql/type/definition'; 6 | import { coerceType } from './coerceType'; 7 | import { TypeResolvingContext, TypeArg } from './types'; 8 | 9 | 10 | export class List extends TypeExpression { 11 | static of(inner: TypeArg | TypeExpression) { 12 | return new List(coerceType(inner)); 13 | } 14 | 15 | constructor(protected innerExp: TypeExpression) { 16 | super(); 17 | } 18 | 19 | getTypeName(): string { 20 | throw new Error(`Cannot resolve name: List is not a GraphQLNamedType`); 21 | } 22 | 23 | getType(context: TypeResolvingContext): GraphQLType { 24 | return new GraphQLList(this.innerExp.getType(context)); 25 | } 26 | getInstantiator(context: TypeResolvingContext): Instantiator { 27 | const innerInstantiator = this.innerExp.getInstantiator(context); 28 | return (values: any[]) => values.map(innerInstantiator); 29 | } 30 | } 31 | 32 | export class NonNull extends TypeExpression { 33 | static of(inner: TypeArg | TypeExpression) { 34 | return new NonNull(coerceType(inner)); 35 | } 36 | 37 | constructor(protected innerExp: TypeExpression) { super(); } 38 | 39 | getTypeName(): string { 40 | throw new Error(`Cannot resolve name: NonNull is not a GraphQLNamedType`); 41 | } 42 | getType(context: TypeResolvingContext): GraphQLType { 43 | return new GraphQLNonNull(this.innerExp.getType(context)); 44 | } 45 | getInstantiator(context: TypeResolvingContext): Instantiator { 46 | return this.innerExp.getInstantiator(context); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/type-expression/types.ts: -------------------------------------------------------------------------------- 1 | import { MetadataStorage } from '../metadata/MetadataStorage'; 2 | import { DefinitionKind } from '../metadata/Definition'; 3 | import { GraphQLType } from 'graphql'; 4 | 5 | 6 | export interface TypeResolvingContext { 7 | storage: MetadataStorage; 8 | kind: DefinitionKind; 9 | } 10 | 11 | export type TypeArg = GraphQLType | Function | string | Object; 12 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type ResolverMap = Object; 2 | 3 | export type ResolvedValue = T | Promise; 4 | export type ResolvedList = T[] | Promise | Promise[]; 5 | 6 | export interface ConcreteClass { 7 | new(...args: any[]): T; 8 | } 9 | 10 | export type Instantiator = (value: { [key: string]: any }) => TClass; 11 | 12 | export function defaultInputFieldInstantiator(value: any) { 13 | return value; 14 | } 15 | 16 | export type Thunk = () => T; 17 | -------------------------------------------------------------------------------- /src/utilities/ResolverContext.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from 'graphql'; 2 | 3 | 4 | /** 5 | * Class-based resolver context 6 | */ 7 | export class ResolverContext { 8 | public $env: IArguments; 9 | public get $source(): TSource { 10 | return this.$env[0]; 11 | } 12 | public get $context(): TContext { 13 | return this.$env[1]; 14 | } 15 | public get $info(): GraphQLResolveInfo { 16 | return this.$env[2]; 17 | } 18 | 19 | constructor(_base: TSource, _context?: TContext, _info?: GraphQLResolveInfo) { 20 | this.$env = arguments; 21 | } 22 | } 23 | 24 | /** 25 | * Decorator for fields resolved from source directly 26 | * @param fieldName 27 | */ 28 | export function source(fieldName?: string) { 29 | return function(prototype: { $source: any }, propertyKey: string) { 30 | const get = function(this: any) { 31 | return this.$source[fieldName || propertyKey]; 32 | }; 33 | const set = function(this: any, value: any) { 34 | this.$source[fieldName || propertyKey] = value; 35 | }; 36 | Object.defineProperty(prototype, propertyKey, { get, set }); 37 | }; 38 | } 39 | 40 | /** 41 | * Decorator for field resolved after async fetching 42 | * prototype should have `$fetch()` method which returns a Promise 43 | * @param fieldName 44 | */ 45 | export function lazy(fieldName?: string) { 46 | return function(prototype: { $fetch: () => Promise }, propertyKey: string) { 47 | const get = function(this: any) { 48 | if (!this.$__fetcher__) { 49 | this.$__fetcher__ = this.$fetch(); 50 | } 51 | return this.$__fetcher__.then((remoteSource: any) => remoteSource[fieldName || propertyKey]); 52 | }; 53 | Object.defineProperty(prototype, propertyKey, { get }); 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/utilities/formatObjectInfo.ts: -------------------------------------------------------------------------------- 1 | export function formatObjectInfo(obj: any): string { 2 | let name: string; 3 | let type: string; 4 | try { 5 | if (typeof obj === 'function') { 6 | name = obj.name || '[anonymous function]'; 7 | type = typeof obj; 8 | } else { 9 | name = String(obj); 10 | type = obj && obj.constructor.name; 11 | } 12 | return `${name}<${type}>`; 13 | } catch (e) { 14 | return String(obj); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/__snapshots__/basic_mutation.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Basic mutation and schema generation generates schema as expected 1`] = ` 4 | "type Query { 5 | getMember: Member! 6 | } 7 | 8 | type Member { 9 | id: Int! 10 | name: String! 11 | email: String! 12 | } 13 | 14 | type Mutation { 15 | createMember(name: String!, email: String!): Member! 16 | } 17 | " 18 | `; 19 | -------------------------------------------------------------------------------- /tests/__snapshots__/basic_query.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Basic queries and schema generation generates schema as expected 1`] = ` 4 | "type Query { 5 | test: Test 6 | erroneousTest: Test 7 | testPassingSource: Test 8 | } 9 | 10 | type Test { 11 | \\"\\"\\"description1\\"\\"\\" 12 | resolverGotDefinitionInstance: Boolean! 13 | greeting(greeting: String, name: String!): String! 14 | bigGreeting(greeting: String, name: String!): String! 15 | fieldWithDefaultResolver: String 16 | } 17 | " 18 | `; 19 | -------------------------------------------------------------------------------- /tests/__snapshots__/field_with_graphqltype.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`field with GraphQLType generates schema as expected 1`] = ` 4 | "type Query { 5 | item: Item 6 | nonNullItem: Item! 7 | baz: Baz 8 | erroneousBaz: Baz 9 | } 10 | 11 | type Item { 12 | name: String 13 | info: ItemInfo 14 | } 15 | 16 | type ItemInfo { 17 | description: String 18 | } 19 | 20 | enum Baz { 21 | A 22 | B 23 | C 24 | } 25 | " 26 | `; 27 | -------------------------------------------------------------------------------- /tests/__snapshots__/input_type.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Input type generates schema and works as expected 1`] = ` 4 | "type Query { 5 | formatFullName(input: NameInput): String! 6 | personInputInstantiated(person: PersonInput): Boolean! 7 | personNonNullInputWorks(person: PersonInput!): Boolean! 8 | personNonNullListInputWorks(people: [PersonInput!]): Boolean! 9 | echoPerson(person: PersonInput): Person! 10 | tenGroups: [Group]! 11 | } 12 | 13 | input NameInput { 14 | firstName: String! 15 | lastName: String! 16 | } 17 | 18 | input PersonInput { 19 | address: String! 20 | name: NameInput 21 | } 22 | 23 | type Person { 24 | address: String! 25 | name: Name 26 | } 27 | 28 | type Name { 29 | firstName: String! 30 | lastName: String! 31 | } 32 | 33 | type Group { 34 | echoPerson(person: PersonInput): Person! 35 | } 36 | " 37 | `; 38 | -------------------------------------------------------------------------------- /tests/__snapshots__/recursive_type.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Schema generation and query of recursive types generates schema as expected 1`] = ` 4 | "type Query { 5 | getMember(id: Int!): Member 6 | } 7 | 8 | type Member { 9 | id: Int! 10 | name: String! 11 | email: String! 12 | friend: Member 13 | } 14 | " 15 | `; 16 | -------------------------------------------------------------------------------- /tests/__snapshots__/root_types_and_extensions.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Root types and extensions should generate schema as expected 1`] = ` 4 | "type User { 5 | id: Int! 6 | username: String! 7 | } 8 | 9 | type Query { 10 | getUser(userId: Int!): User! 11 | userClassName: String! 12 | } 13 | 14 | type Mutation { 15 | createUser(user: UserInput!): User! 16 | } 17 | 18 | input UserInput { 19 | username: String! 20 | } 21 | " 22 | `; 23 | -------------------------------------------------------------------------------- /tests/basic_mutation.test.ts: -------------------------------------------------------------------------------- 1 | import { graphql, GraphQLSchema, printSchema } from 'graphql'; 2 | 3 | import { getType, gql, defineType } from '../src'; 4 | 5 | 6 | @defineType(gql` 7 | type Member { 8 | id: Int! 9 | name: String! 10 | email: String! 11 | } 12 | `) 13 | class Member { 14 | constructor( 15 | public id: number, 16 | public name: string, 17 | public email: string, 18 | ) { } 19 | } 20 | 21 | @defineType(gql` 22 | type Query { 23 | getMember: ${Member}! @resolver 24 | } 25 | `) 26 | class Query { 27 | public static getMember() { 28 | return new Member(1, 'Jonghyun', 'j@example.com'); 29 | } 30 | } 31 | 32 | @defineType(gql` 33 | type Mutation { 34 | createMember(name: String!, email: String!): ${Member}! 35 | } 36 | `) 37 | class Mutation { 38 | public static createMember(_source: null, { name, email }: { name: string, email: string }) { 39 | return new Member(2, name, email); 40 | } 41 | } 42 | 43 | const schema = new GraphQLSchema({ 44 | query: getType(Query), 45 | mutation: getType(Mutation), 46 | }); 47 | 48 | 49 | describe('Basic mutation and schema generation', () => { 50 | 51 | it('generates schema as expected', () => { 52 | expect(printSchema(schema)).toMatchSnapshot(); 53 | }); 54 | 55 | it('passes source and args to its resolver', async () => { 56 | const result = await graphql({ schema, source: ` 57 | mutation { 58 | createMember(name: "Key" email: "k@example.com") { 59 | id 60 | name 61 | email 62 | } 63 | } 64 | `}); 65 | expect(result).toEqual({ data: { 66 | createMember: { id: 2, name: 'Key', email: 'k@example.com' } 67 | }}); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/basic_query.test.ts: -------------------------------------------------------------------------------- 1 | import { graphql, GraphQLSchema, printSchema } from 'graphql'; 2 | 3 | import { getType, gql, defineType } from '../src'; 4 | import { ResolverContext, source } from '../src/utilities/ResolverContext'; 5 | 6 | 7 | interface TestSource { 8 | fieldWithDefaultResolver?: string; 9 | } 10 | 11 | @defineType(gql` 12 | type Test { 13 | """description1""" 14 | resolverGotDefinitionInstance: Boolean! 15 | 16 | greeting(greeting: String, name: String!): String! 17 | bigGreeting(greeting: String, name: String!): String! 18 | fieldWithDefaultResolver: String 19 | } 20 | `) 21 | class Test extends ResolverContext { 22 | public resolverGotDefinitionInstance() { 23 | return this instanceof Test; 24 | } 25 | public greeting({ name, greeting }: { name: string, greeting: string }) { 26 | return `${greeting || 'Hello'}, ${name}`; 27 | } 28 | public bigGreeting(arg: { name: string, greeting: string }) { 29 | return this.greeting(arg) + '!'; 30 | } 31 | 32 | @source() fieldWithDefaultResolver?: string; 33 | } 34 | 35 | @defineType(gql` 36 | type Query { 37 | test: ${Test} 38 | erroneousTest: ${Test} 39 | testPassingSource: ${Test} 40 | } 41 | `) 42 | class Query extends ResolverContext { 43 | public static test() { 44 | return new Test({}); 45 | } 46 | public static testPassingSource() { 47 | return new Test({ fieldWithDefaultResolver: 'Ohayo' }); 48 | } 49 | } 50 | 51 | const schema = new GraphQLSchema({ query: getType(Query) }); 52 | 53 | 54 | describe('Basic queries and schema generation', () => { 55 | 56 | it('generates schema as expected', () => { 57 | expect(printSchema(schema)).toMatchSnapshot(); 58 | }); 59 | 60 | it('passes source and args to its resolver', async () => { 61 | let result = await graphql({ schema, source: ` 62 | query { 63 | test { 64 | greeting(name: "Suzuki") 65 | bigGreeting(name: "Suzuki") 66 | fieldWithDefaultResolver 67 | } 68 | testPassingSource { 69 | fieldWithDefaultResolver 70 | } 71 | } 72 | `}); 73 | expect(result).toEqual({ data: { 74 | test: { 75 | greeting: 'Hello, Suzuki', 76 | bigGreeting: 'Hello, Suzuki!', 77 | fieldWithDefaultResolver: null, 78 | }, 79 | testPassingSource: { 80 | fieldWithDefaultResolver: 'Ohayo', 81 | }, 82 | } }); 83 | 84 | result = await graphql({ schema, source: ` 85 | query { 86 | test { 87 | resolverGotDefinitionInstance 88 | } 89 | } 90 | `}); 91 | expect(result).toEqual({ data : { 92 | test: { 93 | resolverGotDefinitionInstance: true 94 | } 95 | }}); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /tests/basic_subscriptions.test.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, subscribe, parse } from 'graphql'; 2 | import { defineType, gql, getType, Query } from '../src'; 3 | 4 | 5 | @defineType(gql` 6 | extend type Subscription { 7 | inputIsInstantiated(foo: ${Foo}): Boolean 8 | } 9 | input Foo { 10 | field: Int 11 | } 12 | `) 13 | class Foo { 14 | static async* inputIsInstantiated(_source: null, args: { foo: Foo }) { 15 | for (const val of [args.foo instanceof this]) { 16 | yield val; 17 | } 18 | } 19 | 20 | field: number; 21 | } 22 | 23 | @defineType(gql` 24 | type Subscription { 25 | countUp(from: Int): Int 26 | } 27 | 28 | extend type Query { 29 | hello: String 30 | } 31 | `) 32 | class Subscription { 33 | static async *countUp(_source: null, args: { from: number }) { 34 | for (let i = args.from || 0; i < 3; i++) { 35 | yield i; 36 | } 37 | } 38 | 39 | static hello() { return 'subscriptions'; } 40 | } 41 | 42 | const schema = new GraphQLSchema({ 43 | query: getType(Query), 44 | subscription: getType(Subscription), 45 | types: [getType(Foo)], 46 | }); 47 | 48 | describe('Subscription', () => { 49 | it('can be subscribed', async () => { 50 | const subsFromZero = await subscribe({ schema, document: parse(` 51 | subscription { 52 | countUp 53 | } 54 | `)}) as AsyncIterator; 55 | 56 | const subsFromOne = await subscribe({ schema, document: parse(` 57 | subscription { 58 | countUp(from: 1) 59 | } 60 | `)}) as AsyncIterator; 61 | 62 | expect(await subsFromZero.next()).toEqual({ 63 | done: false, value: { data: { countUp: 0 } }, 64 | }); 65 | expect(await subsFromZero.next()).toEqual({ 66 | done: false, value: { data: { countUp: 1 } }, 67 | }); 68 | expect(await subsFromZero.next()).toEqual({ 69 | done: false, value: { data: { countUp: 2 } }, 70 | }); 71 | expect(await subsFromZero.next()).toEqual({ 72 | done: true, 73 | }); 74 | 75 | expect(await subsFromOne.next()).toEqual({ 76 | done: false, value: { data: { countUp: 1 } }, 77 | }); 78 | expect(await subsFromOne.next()).toEqual({ 79 | done: false, value: { data: { countUp: 2 } }, 80 | }); 81 | expect(await subsFromOne.next()).toEqual({ 82 | done: true, 83 | }); 84 | }); 85 | 86 | it('instantiates input objects', async () => { 87 | const subs = await subscribe({ schema, document: parse(` 88 | subscription { 89 | inputIsInstantiated(foo: { field: 12 }) 90 | } 91 | `)}) as AsyncIterator; 92 | 93 | expect(await subs.next()).toEqual({ 94 | done: false, value: { data: { inputIsInstantiated: true } }, 95 | }); 96 | expect(await subs.next()).toEqual({ 97 | done: true, 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /tests/field_with_graphqltype.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLSchema, 5 | graphql, 6 | printSchema, 7 | GraphQLEnumType 8 | } from 'graphql'; 9 | 10 | import { getType, gql, defineType } from '../src'; 11 | 12 | 13 | @defineType(gql` 14 | type ItemInfo { 15 | description: String 16 | } 17 | `) 18 | class ItemInfo { 19 | description() { 20 | return 'foobarbaz'; 21 | } 22 | } 23 | 24 | const itemType = new GraphQLObjectType({ 25 | name: 'Item', 26 | fields() { 27 | return { 28 | name: { 29 | type: GraphQLString, 30 | resolve() { 31 | return 'foo'; 32 | } 33 | }, 34 | info: { 35 | type: getType(ItemInfo), 36 | resolve() { return new ItemInfo(); } 37 | } 38 | }; 39 | }, 40 | }); 41 | 42 | const bazEnum = new GraphQLEnumType({ 43 | name: 'Baz', 44 | values: { A: {}, B: {}, C: {}, } 45 | }); 46 | 47 | @defineType(gql` 48 | type Query { 49 | item: ${itemType} 50 | nonNullItem: ${itemType}! 51 | baz: ${bazEnum} 52 | erroneousBaz: ${bazEnum} 53 | } 54 | `) 55 | class Query { 56 | static item() { return {}; } 57 | static nonNullItem() { return {}; } 58 | static baz(): 'A' | 'B' | 'C' { 59 | return 'A'; 60 | } 61 | static erroneousBaz(): 'A' | 'B' | 'C' { 62 | return 'D' as any; 63 | } 64 | } 65 | 66 | const schema = new GraphQLSchema({ 67 | query: getType(Query) 68 | }); 69 | 70 | describe('field with GraphQLType', () => { 71 | test('generates schema as expected', () => { 72 | expect(printSchema(schema)).toMatchSnapshot(); 73 | }); 74 | 75 | test('query', async () => { 76 | const result = await graphql({ schema, source: ` 77 | query { 78 | item { 79 | name 80 | info { description } 81 | } 82 | nonNullItem { 83 | name 84 | info { description } 85 | } 86 | } 87 | `}); 88 | expect(result.data!.item.name).toEqual('foo'); 89 | expect(result.data!.item.info.description).toEqual('foobarbaz'); 90 | expect(result.data!.nonNullItem.name).toEqual('foo'); 91 | expect(result.data!.nonNullItem.info.description).toEqual('foobarbaz'); 92 | }); 93 | 94 | test('query enum', async () => { 95 | let result: any; 96 | 97 | result = await graphql({ schema, source: ` 98 | query { 99 | baz 100 | } 101 | `}); 102 | expect(result.data!.baz).toEqual('A'); 103 | 104 | result = await graphql({ schema, source: ` 105 | query { 106 | erroneousBaz 107 | } 108 | `}); 109 | expect(result.errors[0].message).toEqual('Enum \"Baz\" cannot represent value: \"D\"'); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /tests/input_type.test.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, graphql, printSchema } from 'graphql'; 2 | 3 | import { getType, gql, defineType } from '../src'; 4 | 5 | 6 | @defineType(gql` 7 | input NameInput { 8 | firstName: String! 9 | lastName: String! 10 | } 11 | type Name { 12 | firstName: String! 13 | lastName: String! 14 | } 15 | `) 16 | class Name { 17 | 18 | firstName: string; 19 | lastName: string; 20 | 21 | get fullName() { 22 | return `${this.firstName} ${this.lastName}`; 23 | } 24 | } 25 | 26 | let instantiationCount = 0; 27 | 28 | @defineType(gql` 29 | input PersonInput { 30 | address: String! 31 | name: ${Name} 32 | } 33 | type Person { 34 | address: String! 35 | name: ${Name} 36 | } 37 | `) 38 | class Person { 39 | address: string; 40 | name: Name; 41 | constructor() { 42 | instantiationCount ++; 43 | } 44 | } 45 | 46 | @defineType(gql` 47 | type Group { 48 | echoPerson(person: ${Person}): ${Person}! 49 | } 50 | `) 51 | class Group { 52 | echoPerson(args: { person: Person }) { 53 | return args.person; 54 | } 55 | } 56 | 57 | @defineType(gql` 58 | type Query { # resolved to NameInput 59 | formatFullName(input: ${Name}): String! 60 | # resolved to PersonInput 61 | personInputInstantiated(person: ${Person}): Boolean! 62 | personNonNullInputWorks(person: ${Person}!): Boolean! 63 | personNonNullListInputWorks(people: [${Person}!]): Boolean! 64 | 65 | echoPerson(person: ${Person}): ${Person}! # resolved to Person 66 | tenGroups: [${Group}]! 67 | } 68 | `) 69 | class Query { 70 | static formatFullName(_source: null, args: { input: Name }) { 71 | return args.input.fullName; 72 | } 73 | static personInputInstantiated(_source: null, args: { person: Person }) { 74 | return (args.person instanceof Person) && (args.person.name instanceof Name); 75 | } 76 | static personNonNullInputWorks(_source: null, args: { person: Person }) { 77 | return (args.person instanceof Person) && (args.person.name instanceof Name); 78 | } 79 | static personNonNullListInputWorks(_source: null, args: { people: Person[] }) { 80 | return args.people.reduce((res, person) => { 81 | return res && (person instanceof Person) && (person.name instanceof Name); 82 | }, true); 83 | } 84 | static echoPerson(_source: null, args: { person: Person }) { 85 | return args.person; 86 | } 87 | static tenGroups() { 88 | const groups = []; 89 | for (let i = 0; i < 10; i++) { 90 | groups.push(new Group()); 91 | } 92 | return groups; 93 | } 94 | } 95 | 96 | const schema = new GraphQLSchema({ 97 | query: getType(Query), 98 | }); 99 | 100 | describe('Input type', () => { 101 | it('generates schema and works as expected', async () => { 102 | 103 | expect(printSchema(schema)).toMatchSnapshot(); 104 | 105 | const results = await graphql({ schema, source: ` 106 | query { 107 | formatFullName(input: { firstName: "Foo", lastName: "Bar" }) 108 | personInputInstantiated(person: { 109 | address: "A", 110 | name: { 111 | firstName: "Foo", 112 | lastName: "Bar" 113 | } 114 | }) 115 | personNonNullInputWorks(person: { 116 | address: "A", 117 | name: { 118 | firstName: "Foo", 119 | lastName: "Bar" 120 | } 121 | }) 122 | personNonNullListInputWorks(people: [ 123 | { 124 | address: "A", 125 | name: { 126 | firstName: "Foo", 127 | lastName: "Bar" 128 | } 129 | }, 130 | { 131 | address: "B", 132 | name: { 133 | firstName: "Foo", 134 | lastName: "Bar" 135 | } 136 | } 137 | ]) 138 | 139 | echoPerson(person: { 140 | address: "A", 141 | name: { 142 | firstName: "Foo", 143 | lastName: "Bar" 144 | } 145 | }) { 146 | address 147 | name { 148 | firstName 149 | lastName 150 | } 151 | } 152 | } 153 | ` }); 154 | 155 | expect(results).toEqual({ data: { 156 | formatFullName: 'Foo Bar', 157 | personInputInstantiated: true, 158 | personNonNullInputWorks: true, 159 | personNonNullListInputWorks: true, 160 | echoPerson: { 161 | address: 'A', 162 | name: { 163 | firstName: 'Foo', 164 | lastName: 'Bar' 165 | } 166 | } 167 | } }); 168 | }); 169 | 170 | it('should not instantiate input type which is already cached', async () => { 171 | instantiationCount = 0; 172 | await graphql({ schema, source: ` 173 | query($person: PersonInput) { 174 | tenGroups { 175 | echoPerson(person: $person) { 176 | address 177 | } 178 | } 179 | } 180 | `, variableValues: { 181 | person: { 182 | address: 'A', 183 | name: { 184 | firstName: 'Foo', 185 | lastName: 'Bar' 186 | } 187 | }, 188 | }}); // giving argument as variable lets us avoid extra instantiations 189 | expect(instantiationCount).toBe(1); 190 | }); 191 | }); 192 | -------------------------------------------------------------------------------- /tests/plain_resolvermap.test.ts: -------------------------------------------------------------------------------- 1 | import { defineType, gql, getType } from '../src'; 2 | import { GraphQLSchema, graphql } from 'graphql'; 3 | 4 | 5 | 6 | test('Type definition with plain resolver map', async () => { 7 | 8 | const QueryType = defineType(() => gql` 9 | type Query { 10 | hello: String 11 | post(id: String!): ${PostType}! 12 | } 13 | `)({ 14 | hello() { 15 | return 'World'; 16 | }, 17 | post(_source: null, args: { id: string }) { 18 | return { 19 | id: args.id, 20 | title: `Post: ${args.id}`, 21 | }; 22 | } 23 | }); 24 | 25 | const PostType = defineType(() => gql` 26 | type Post { 27 | id: String! 28 | title: String! 29 | } 30 | `)({}); 31 | 32 | const schema = new GraphQLSchema({ query: getType(QueryType) }); 33 | let res = await graphql({ schema, source: `{ hello }` }); 34 | expect(res).toEqual({ data: { hello: 'World' } }); 35 | 36 | res = await graphql({ schema, source: `{ post(id: "1121") { id, title } }`}); 37 | expect(res).toEqual({ data: { post: { id: '1121', title: 'Post: 1121' } } }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/recursive_type.test.ts: -------------------------------------------------------------------------------- 1 | import { graphql, GraphQLSchema, printSchema } from 'graphql'; 2 | 3 | import { getType, gql, defineType } from '../src'; 4 | 5 | 6 | interface MemberSource { 7 | id: number; 8 | name: string; 9 | email: string; 10 | friendId: number; 11 | } 12 | 13 | const members: MemberSource[] = [ 14 | { id: 0, name: 'Foo', email: 'foo@example.com', friendId: 1, }, 15 | { id: 1, name: 'Bar', email: 'bar@example.com', friendId: 0 }, 16 | ]; 17 | 18 | @defineType(gql` 19 | type Member { 20 | id: Int! 21 | name: String! 22 | email: String! 23 | friend: ${Member} 24 | } 25 | `) 26 | class Member { 27 | id: number; 28 | name: string; 29 | email: string; 30 | 31 | private friendId: number; 32 | 33 | friend() { 34 | return Member.find(this.friendId); 35 | } 36 | 37 | static find(id: number) { 38 | const member = members.find(m => m.id === id); 39 | return member && Object.assign(new Member, member); 40 | } 41 | } 42 | 43 | @defineType(gql` 44 | type Query { 45 | getMember(id: Int!): ${Member} 46 | } 47 | `) 48 | class Query { 49 | public static getMember(source: null, { id }: { id: number }) { 50 | return Member.find(id); 51 | } 52 | } 53 | 54 | const schema = new GraphQLSchema({ query: getType(Query) }); 55 | 56 | describe('Schema generation and query of recursive types', () => { 57 | 58 | it('generates schema as expected', () => { 59 | expect(printSchema(schema)).toMatchSnapshot(); 60 | }); 61 | 62 | it('passes source and args to its resolver', async () => { 63 | const result = await graphql({ schema, source: ` 64 | query { 65 | getMember(id: 0) { 66 | id 67 | friend { 68 | id 69 | friend { 70 | id 71 | } 72 | } 73 | } 74 | } 75 | `}); 76 | expect(result).toEqual({ data: { 77 | getMember: { 78 | id: 0, 79 | friend: { 80 | id: 1, 81 | friend: { 82 | id: 0 83 | } 84 | } 85 | } 86 | }}); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /tests/root_types_and_extensions.test.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, printSchema, graphql } from 'graphql'; 2 | import { defineType, gql, Mutation, Query, getType } from '../src'; 3 | 4 | 5 | @defineType(gql` 6 | type User { 7 | id: Int! 8 | username: String! 9 | } 10 | input UserInput { 11 | username: String! 12 | } 13 | 14 | extend type Query { 15 | getUser(userId: Int!): User! 16 | userClassName: String! 17 | } 18 | extend type Mutation { 19 | createUser(user: UserInput!): User! 20 | } 21 | `) 22 | class User { 23 | id: number; 24 | username: string; 25 | 26 | // extend type Query 27 | static getUser(_source: null, { userId }: { userId: number }) { 28 | const user = new User(); 29 | user.id = userId; 30 | user.username = `User${userId}`; 31 | return user; 32 | } 33 | 34 | static userClassName() { 35 | return this.name; 36 | } 37 | 38 | // extend type Mutation 39 | static createUser(_source: null, { user }: { user: User }) { 40 | return user; 41 | } 42 | } 43 | 44 | const schema = new GraphQLSchema({ 45 | query: getType(Query), 46 | mutation: getType(Mutation), 47 | types: [getType(User)], 48 | }); 49 | 50 | 51 | describe('Root types and extensions', () => { 52 | it('should generate schema as expected', () => { 53 | expect(printSchema(schema)).toMatchSnapshot(); 54 | }); 55 | 56 | it('has resolvers binded to their definition class', async () => { 57 | const res = await graphql({ schema, source: ` 58 | query { 59 | getUser(userId: 12) { 60 | id 61 | username 62 | } 63 | userClassName 64 | } 65 | `}); 66 | expect(res).toEqual({ 67 | data: { 68 | getUser: { 69 | id: 12, 70 | username: 'User12', 71 | }, 72 | userClassName: 'User', 73 | }, 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/starwars-relay/ArrayConnection.ts: -------------------------------------------------------------------------------- 1 | import { Connection, ConnectionArguments } from 'cursor-connection'; 2 | import { getOffsetWithDefault, offsetToCursor } from 'graphql-relay'; 3 | 4 | 5 | export abstract class ArrayConnection extends Connection { 6 | constructor(protected array: TEdgeSource[], args: ConnectionArguments) { 7 | super(args); 8 | 9 | const { after, before, first, last } = args; 10 | 11 | const arrayLength = array.length; 12 | const beforeOffset = getOffsetWithDefault(before, arrayLength); 13 | const afterOffset = getOffsetWithDefault(after, -1); 14 | 15 | let startOffset = Math.max(afterOffset + 1, 0); 16 | let endOffset = Math.min(beforeOffset, arrayLength); 17 | if (typeof first === 'number') { 18 | endOffset = Math.min(endOffset, startOffset + first); 19 | } 20 | if (typeof last === 'number') { 21 | startOffset = Math.max(startOffset, endOffset - last); 22 | } 23 | this.startOffset = startOffset; 24 | this.endOffset = endOffset; 25 | this.afterOffset = afterOffset; 26 | this.beforeOffset = beforeOffset; 27 | } 28 | 29 | private startOffset: number; 30 | private endOffset: number; 31 | private afterOffset: number; 32 | private beforeOffset: number; 33 | 34 | getEdgeSources() { 35 | return this.array.slice(this.startOffset, this.endOffset) 36 | .map((source, index) => ({ source, index: index + this.startOffset })); 37 | } 38 | 39 | resolveCursor(source: { index: number }) { 40 | return offsetToCursor(source.index); 41 | } 42 | 43 | resolveHasNextPage() { 44 | const { args, endOffset, beforeOffset, array } = this; 45 | 46 | const upperBound = args.before ? beforeOffset : array.length; 47 | return typeof args.first === 'number' ? endOffset < upperBound : false; 48 | } 49 | 50 | resolveHasPreviousPage() { 51 | const { args, startOffset, afterOffset } = this; 52 | 53 | const lowerBound = args.after ? afterOffset + 1 : 0; 54 | return typeof args.last === 'number' ? startOffset > lowerBound : false; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/starwars-relay/starWarsConnection.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import { StarWarsSchema } from './starWarsSchema'; 11 | import { graphql } from 'graphql'; 12 | 13 | // 80+ char lines are useful in describe/it, so ignore in this file. 14 | /* eslint-disable max-len */ 15 | 16 | describe('Star Wars connections', () => { 17 | it('fetches the first ship of the rebels', async () => { 18 | const query = ` 19 | query RebelsShipsQuery { 20 | rebels { 21 | name, 22 | ships(first: 1) { 23 | edges { 24 | node { 25 | name 26 | } 27 | } 28 | } 29 | } 30 | } 31 | `; 32 | const expected = { 33 | rebels: { 34 | name: 'Alliance to Restore the Republic', 35 | ships: { 36 | edges: [ 37 | { 38 | node: { 39 | name: 'X-Wing' 40 | } 41 | } 42 | ] 43 | } 44 | } 45 | }; 46 | const result = await graphql(StarWarsSchema, query); 47 | expect(result).toEqual({ data: expected }); 48 | }); 49 | 50 | it('fetches the first two ships of the rebels with a cursor', async () => { 51 | const query = ` 52 | query MoreRebelShipsQuery { 53 | rebels { 54 | name, 55 | ships(first: 2) { 56 | edges { 57 | cursor, 58 | node { 59 | name 60 | } 61 | } 62 | } 63 | } 64 | } 65 | `; 66 | const expected = { 67 | rebels: { 68 | name: 'Alliance to Restore the Republic', 69 | ships: { 70 | edges: [ 71 | { 72 | cursor: 'YXJyYXljb25uZWN0aW9uOjA=', 73 | node: { 74 | name: 'X-Wing' 75 | } 76 | }, 77 | { 78 | cursor: 'YXJyYXljb25uZWN0aW9uOjE=', 79 | node: { 80 | name: 'Y-Wing' 81 | } 82 | } 83 | ] 84 | } 85 | } 86 | }; 87 | const result = await graphql(StarWarsSchema, query); 88 | expect(result).toEqual({ data: expected }); 89 | }); 90 | 91 | it('fetches the next three ships of the rebels with a cursor', async () => { 92 | const query = ` 93 | query EndOfRebelShipsQuery { 94 | rebels { 95 | name, 96 | ships(first: 3 after: "YXJyYXljb25uZWN0aW9uOjE=") { 97 | edges { 98 | cursor, 99 | node { 100 | name 101 | } 102 | } 103 | } 104 | } 105 | } 106 | `; 107 | const expected = { 108 | rebels: { 109 | name: 'Alliance to Restore the Republic', 110 | ships: { 111 | edges: [ 112 | { 113 | cursor: 'YXJyYXljb25uZWN0aW9uOjI=', 114 | node: { 115 | name: 'A-Wing' 116 | } 117 | }, 118 | { 119 | cursor: 'YXJyYXljb25uZWN0aW9uOjM=', 120 | node: { 121 | name: 'Millenium Falcon' 122 | } 123 | }, 124 | { 125 | cursor: 'YXJyYXljb25uZWN0aW9uOjQ=', 126 | node: { 127 | name: 'Home One' 128 | } 129 | } 130 | ] 131 | } 132 | } 133 | }; 134 | const result = await graphql(StarWarsSchema, query); 135 | expect(result).toEqual({ data: expected }); 136 | }); 137 | 138 | it('fetches no ships of the rebels at the end of connection', async () => { 139 | const query = ` 140 | query RebelsQuery { 141 | rebels { 142 | name, 143 | ships(first: 3 after: "YXJyYXljb25uZWN0aW9uOjQ=") { 144 | edges { 145 | cursor, 146 | node { 147 | name 148 | } 149 | } 150 | } 151 | } 152 | } 153 | `; 154 | const expected = { 155 | rebels: { 156 | name: 'Alliance to Restore the Republic', 157 | ships: { 158 | edges: [] 159 | } 160 | } 161 | }; 162 | const result = await graphql(StarWarsSchema, query); 163 | expect(result).toEqual({ data: expected }); 164 | }); 165 | 166 | it('identifies the end of the list', async () => { 167 | const query = ` 168 | query EndOfRebelShipsQuery { 169 | rebels { 170 | name, 171 | originalShips: ships(first: 2) { 172 | edges { 173 | node { 174 | name 175 | } 176 | } 177 | pageInfo { 178 | hasNextPage 179 | } 180 | } 181 | moreShips: ships(first: 3 after: "YXJyYXljb25uZWN0aW9uOjE=") { 182 | edges { 183 | node { 184 | name 185 | } 186 | } 187 | pageInfo { 188 | hasNextPage 189 | } 190 | } 191 | } 192 | } 193 | `; 194 | const expected = { 195 | rebels: { 196 | name: 'Alliance to Restore the Republic', 197 | originalShips: { 198 | edges: [ 199 | { 200 | node: { 201 | name: 'X-Wing' 202 | } 203 | }, 204 | { 205 | node: { 206 | name: 'Y-Wing' 207 | } 208 | } 209 | ], 210 | pageInfo: { 211 | hasNextPage: true 212 | } 213 | }, 214 | moreShips: { 215 | edges: [ 216 | { 217 | node: { 218 | name: 'A-Wing' 219 | } 220 | }, 221 | { 222 | node: { 223 | name: 'Millenium Falcon' 224 | } 225 | }, 226 | { 227 | node: { 228 | name: 'Home One' 229 | } 230 | } 231 | ], 232 | pageInfo: { 233 | hasNextPage: false 234 | } 235 | } 236 | } 237 | }; 238 | const result = await graphql(StarWarsSchema, query); 239 | expect(result).toEqual({ data: expected }); 240 | }); 241 | }); -------------------------------------------------------------------------------- /tests/starwars-relay/starWarsData.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | /** 9 | * This defines a basic set of data for our Star Wars Schema. 10 | * 11 | * This data is hard coded for the sake of the demo, but you could imagine 12 | * fetching this data from a backend service rather than from hardcoded 13 | * JSON objects in a more complex demo. 14 | */ 15 | 16 | const xwing: ShipSource = { 17 | id: '1', 18 | name: 'X-Wing', 19 | }; 20 | 21 | const ywing: ShipSource = { 22 | id: '2', 23 | name: 'Y-Wing', 24 | }; 25 | 26 | const awing: ShipSource = { 27 | id: '3', 28 | name: 'A-Wing', 29 | }; 30 | 31 | // Yeah, technically it's Corellian. But it flew in the service of the rebels, 32 | // so for the purposes of this demo it's a rebel ship. 33 | const falcon: ShipSource = { 34 | id: '4', 35 | name: 'Millenium Falcon', 36 | }; 37 | 38 | const homeOne: ShipSource = { 39 | id: '5', 40 | name: 'Home One', 41 | }; 42 | 43 | const tieFighter: ShipSource = { 44 | id: '6', 45 | name: 'TIE Fighter', 46 | }; 47 | 48 | const tieInterceptor: ShipSource = { 49 | id: '7', 50 | name: 'TIE Interceptor', 51 | }; 52 | 53 | const executor: ShipSource = { 54 | id: '8', 55 | name: 'Executor', 56 | }; 57 | 58 | const rebels: FactionSource = { 59 | id: '1', 60 | name: 'Alliance to Restore the Republic', 61 | ships: [ '1', '2', '3', '4', '5' ] 62 | }; 63 | 64 | const empire: FactionSource = { 65 | id: '2', 66 | name: 'Galactic Empire', 67 | ships: [ '6', '7', '8' ] 68 | }; 69 | 70 | const data: { 71 | Faction: { [id: string]: FactionSource }, 72 | Ship: { [id: string]: ShipSource } 73 | } = { 74 | Faction: { 75 | '1': rebels, 76 | '2': empire 77 | }, 78 | Ship: { 79 | '1': xwing, 80 | '2': ywing, 81 | '3': awing, 82 | '4': falcon, 83 | '5': homeOne, 84 | '6': tieFighter, 85 | '7': tieInterceptor, 86 | '8': executor 87 | } 88 | }; 89 | 90 | let nextShip = 9; 91 | 92 | export interface ShipSource { 93 | id: string; 94 | name: string; 95 | } 96 | 97 | export interface FactionSource { 98 | id: string; 99 | name: string; 100 | ships: string[]; 101 | } 102 | 103 | export function createShip(shipName: string, factionId: string): ShipSource { 104 | const newShip = { 105 | id: String(nextShip++), 106 | name: shipName 107 | }; 108 | data.Ship[newShip.id] = newShip; 109 | data.Faction[factionId].ships.push(newShip.id); 110 | return newShip; 111 | } 112 | 113 | export function getShip(id: string): ShipSource { 114 | return data.Ship[id]; 115 | } 116 | 117 | export function getFaction(id: string): FactionSource { 118 | return data.Faction[id]; 119 | } 120 | 121 | export function getRebels(): FactionSource { 122 | return rebels; 123 | } 124 | 125 | export function getEmpire(): FactionSource { 126 | return empire; 127 | } -------------------------------------------------------------------------------- /tests/starwars-relay/starWarsMutation.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import { StarWarsSchema } from './starWarsSchema'; 11 | import { graphql } from 'graphql'; 12 | 13 | // 80+ char lines are useful in describe/it, so ignore in this file. 14 | /* eslint-disable max-len */ 15 | 16 | describe('Star Wars mutations', () => { 17 | it('mutates the data set', async () => { 18 | const mutation = ` 19 | mutation AddBWingQuery($input: IntroduceShipInput!) { 20 | introduceShip(input: $input) { 21 | ship { 22 | id 23 | name 24 | } 25 | faction { 26 | name 27 | } 28 | clientMutationId 29 | } 30 | } 31 | `; 32 | const params = { 33 | input: { 34 | shipName: 'B-Wing', 35 | factionId: '1', 36 | clientMutationId: 'abcde', 37 | } 38 | }; 39 | const expected = { 40 | introduceShip: { 41 | ship: { 42 | id: 'U2hpcDo5', 43 | name: 'B-Wing' 44 | }, 45 | faction: { 46 | name: 'Alliance to Restore the Republic' 47 | }, 48 | clientMutationId: 'abcde', 49 | } 50 | }; 51 | const result = await graphql(StarWarsSchema, mutation, null, null, params); 52 | expect(result).toEqual({ data: expected }); 53 | }); 54 | }); -------------------------------------------------------------------------------- /tests/starwars-relay/starWarsObjectIdentification.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import { StarWarsSchema } from './starWarsSchema'; 11 | import { graphql } from 'graphql'; 12 | 13 | // 80+ char lines are useful in describe/it, so ignore in this file. 14 | /* eslint-disable max-len */ 15 | 16 | describe('Star Wars object identification', () => { 17 | it('fetches the ID and name of the rebels', async () => { 18 | const query = ` 19 | query RebelsQuery { 20 | rebels { 21 | id 22 | name 23 | } 24 | } 25 | `; 26 | const expected = { 27 | rebels: { 28 | id: 'RmFjdGlvbjox', 29 | name: 'Alliance to Restore the Republic' 30 | } 31 | }; 32 | const result = await graphql(StarWarsSchema, query); 33 | expect(result).toEqual({ data: expected }); 34 | }); 35 | 36 | it('refetches the rebels', async () => { 37 | const query = ` 38 | query RebelsRefetchQuery { 39 | node(id: "RmFjdGlvbjox") { 40 | id 41 | ... on Faction { 42 | name 43 | } 44 | } 45 | } 46 | `; 47 | const expected = { 48 | node: { 49 | id: 'RmFjdGlvbjox', 50 | name: 'Alliance to Restore the Republic' 51 | } 52 | }; 53 | const result = await graphql(StarWarsSchema, query); 54 | expect(result).toEqual({ data: expected }); 55 | }); 56 | 57 | it('fetches the ID and name of the empire', async () => { 58 | const query = ` 59 | query EmpireQuery { 60 | empire { 61 | id 62 | name 63 | } 64 | } 65 | `; 66 | const expected = { 67 | empire: { 68 | id: 'RmFjdGlvbjoy', 69 | name: 'Galactic Empire' 70 | } 71 | }; 72 | const result = await graphql(StarWarsSchema, query); 73 | expect(result).toEqual({ data: expected }); 74 | }); 75 | 76 | it('refetches the empire', async () => { 77 | const query = ` 78 | query EmpireRefetchQuery { 79 | node(id: "RmFjdGlvbjoy") { 80 | id 81 | ... on Faction { 82 | name 83 | } 84 | } 85 | } 86 | `; 87 | const expected = { 88 | node: { 89 | id: 'RmFjdGlvbjoy', 90 | name: 'Galactic Empire' 91 | } 92 | }; 93 | const result = await graphql(StarWarsSchema, query); 94 | expect(result).toEqual({ data: expected }); 95 | }); 96 | 97 | it('refetches the X-Wing', async () => { 98 | const query = ` 99 | query XWingRefetchQuery { 100 | node(id: "U2hpcDox") { 101 | id 102 | ... on Ship { 103 | name 104 | } 105 | } 106 | } 107 | `; 108 | const expected = { 109 | node: { 110 | id: 'U2hpcDox', 111 | name: 'X-Wing' 112 | } 113 | }; 114 | const result = await graphql(StarWarsSchema, query); 115 | expect(result).toEqual({ data: expected }); 116 | }); 117 | }); -------------------------------------------------------------------------------- /tests/starwars-relay/starWarsSchema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | import { fromGlobalId, ConnectionArguments, toGlobalId } from 'graphql-relay'; 3 | 4 | import { defineType, gql, getType, Mutation, Node, RelayMutation, defineConnection } from '../../src'; 5 | import { ResolverContext, source } from '../../src/utilities/ResolverContext'; 6 | 7 | import { createShip, ShipSource, FactionSource, getFaction, getRebels, getEmpire, getShip } from './starWarsData'; 8 | import { ArrayConnection } from './ArrayConnection'; 9 | 10 | 11 | @defineType(gql` 12 | """ 13 | A ship in the Star Wars saga 14 | """ 15 | type Ship implements Node { 16 | """ 17 | The id of the object. 18 | """ 19 | id: ID! 20 | """ 21 | The name of the ship. 22 | """ 23 | name: String 24 | } 25 | `) 26 | class Ship extends ResolverContext implements Node { 27 | static getById(id: string) { 28 | const shipSource = getShip(id); 29 | return new Ship(shipSource); 30 | } 31 | 32 | id() { 33 | return toGlobalId('Ship', this.localId); 34 | } 35 | 36 | @source('id') 37 | localId: string; 38 | 39 | @source() name: string; 40 | } 41 | 42 | @defineConnection({ node: Ship }) 43 | class ShipConnection extends ArrayConnection { 44 | resolveNode(edge: { source: string }) { 45 | return Ship.getById(edge.source); 46 | } 47 | } 48 | 49 | @defineType(gql` 50 | type Faction implements Node { 51 | """ 52 | The id of the object. 53 | """ 54 | id: ID! 55 | """ 56 | The name of the faction. 57 | """ 58 | name: String 59 | """ 60 | The ships used by the faction. 61 | """ 62 | ships(first: Int, last: Int, before: String, after: String): ${ShipConnection} 63 | } 64 | `) 65 | class Faction extends ResolverContext implements Node { 66 | static getById(id: string) { 67 | const factionSource = getFaction(id); 68 | return new Faction(factionSource); 69 | } 70 | 71 | id() { 72 | return toGlobalId('Faction', this.localId); 73 | } 74 | 75 | @source('id') localId: string; 76 | 77 | @source() name: string; 78 | ships(args: ConnectionArguments) { 79 | return new ShipConnection(this.$source.ships, args); 80 | } 81 | } 82 | 83 | @defineType(gql` 84 | type Query { 85 | rebels: ${Faction} 86 | empire: ${Faction} 87 | node(id: ID!): ${Node} 88 | } 89 | `) 90 | class Query { 91 | static rebels() { 92 | return new Faction(getRebels()); 93 | } 94 | static empire() { 95 | return new Faction(getEmpire()); 96 | } 97 | static node(_source: null, args: { id: string }) { 98 | const { type, id } = fromGlobalId(args.id); 99 | 100 | if (type === 'Ship') { 101 | return Ship.getById(id); 102 | } 103 | if (type === 'Faction') { 104 | return Faction.getById(id); 105 | } 106 | return null; 107 | } 108 | } 109 | 110 | @defineType(gql` 111 | input IntroduceShipInput { 112 | shipName: String! 113 | factionId: ID! 114 | } 115 | type IntroduceShipPayload { 116 | ship: ${Ship} 117 | faction: ${Faction} 118 | } 119 | extend type Mutation { 120 | introduceShip(input: ${IntroduceShipMutation}): ${IntroduceShipMutation} 121 | } 122 | `) 123 | class IntroduceShipMutation extends RelayMutation { 124 | shipName: string; 125 | factionId: string; 126 | ship: Ship; 127 | 128 | faction() { 129 | return new Faction(getFaction(this.factionId)); 130 | } 131 | 132 | static introduceShip(_source: null, args: { input: IntroduceShipMutation }) { 133 | const container = args.input; 134 | const newShip = createShip(container.shipName, container.factionId); 135 | container.ship = new Ship(newShip); 136 | return container; 137 | } 138 | } 139 | 140 | export const StarWarsSchema = new GraphQLSchema({ 141 | query: getType(Query), 142 | mutation: getType(Mutation), 143 | types: [getType(IntroduceShipMutation)], 144 | }); 145 | -------------------------------------------------------------------------------- /tests/starwars/starWarsData.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow strict 8 | */ 9 | 10 | /** 11 | * This defines a basic set of data for our Star Wars Schema. 12 | * 13 | * This data is hard coded for the sake of the demo, but you could imagine 14 | * fetching this data from a backend service rather than from hardcoded 15 | * JSON objects in a more complex demo. 16 | */ 17 | 18 | const luke: HumanSource = { 19 | type: 'Human', 20 | id: '1000', 21 | name: 'Luke Skywalker', 22 | friendIds: ['1002', '1003', '2000', '2001'], 23 | appearsIn: [4, 5, 6], 24 | homePlanet: 'Tatooine', 25 | }; 26 | 27 | const vader: HumanSource = { 28 | type: 'Human', 29 | id: '1001', 30 | name: 'Darth Vader', 31 | friendIds: ['1004'], 32 | appearsIn: [4, 5, 6], 33 | homePlanet: 'Tatooine', 34 | }; 35 | 36 | const han: HumanSource = { 37 | type: 'Human', 38 | id: '1002', 39 | name: 'Han Solo', 40 | friendIds: ['1000', '1003', '2001'], 41 | appearsIn: [4, 5, 6], 42 | }; 43 | 44 | const leia: HumanSource = { 45 | type: 'Human', 46 | id: '1003', 47 | name: 'Leia Organa', 48 | friendIds: ['1000', '1002', '2000', '2001'], 49 | appearsIn: [4, 5, 6], 50 | homePlanet: 'Alderaan', 51 | }; 52 | 53 | const tarkin: HumanSource = { 54 | type: 'Human', 55 | id: '1004', 56 | name: 'Wilhuff Tarkin', 57 | friendIds: ['1001'], 58 | appearsIn: [4], 59 | }; 60 | 61 | const humanData: { [id: string]: HumanSource } = { 62 | '1000': luke, 63 | '1001': vader, 64 | '1002': han, 65 | '1003': leia, 66 | '1004': tarkin, 67 | }; 68 | 69 | const threepio: DroidSource = { 70 | type: 'Droid', 71 | id: '2000', 72 | name: 'C-3PO', 73 | friendIds: ['1000', '1002', '1003', '2001'], 74 | appearsIn: [4, 5, 6], 75 | primaryFunction: 'Protocol', 76 | }; 77 | 78 | const artoo: DroidSource = { 79 | type: 'Droid', 80 | id: '2001', 81 | name: 'R2-D2', 82 | friendIds: ['1000', '1002', '1003'], 83 | appearsIn: [4, 5, 6], 84 | primaryFunction: 'Astromech', 85 | }; 86 | 87 | const droidData: { [id: string]: DroidSource } = { 88 | '2000': threepio, 89 | '2001': artoo, 90 | }; 91 | 92 | /** 93 | * These are Flow types which correspond to the schema. 94 | * They represent the shape of the data visited during field resolution. 95 | */ 96 | 97 | export type EpisodeValue = 4 | 5 | 6; 98 | 99 | export type CharacterSource = { 100 | type: string; 101 | id: string, 102 | name: string, 103 | friendIds: Array, 104 | appearsIn: Array, 105 | }; 106 | 107 | export type HumanSource = { 108 | type: 'Human', 109 | id: string, 110 | name: string, 111 | friendIds: Array, 112 | appearsIn: Array, 113 | homePlanet?: string, 114 | }; 115 | 116 | export type DroidSource = { 117 | type: 'Droid', 118 | id: string, 119 | name: string, 120 | friendIds: Array, 121 | appearsIn: Array, 122 | primaryFunction: string, 123 | }; 124 | 125 | /** 126 | * Helper function to get a character by ID. 127 | */ 128 | function getCharacter(id: string): Promise { 129 | // Returning a promise just to illustrate GraphQL.js's support. 130 | return Promise.resolve(humanData[id] || droidData[id]); 131 | } 132 | 133 | /** 134 | * Allows us to query for a character's friends. 135 | */ 136 | export function getFriends(character: { friendIds: string [] }) { 137 | // Notice that GraphQL accepts Arrays of Promises. 138 | return character.friendIds.map(id => getCharacter(id)); 139 | } 140 | 141 | /** 142 | * Allows us to fetch the undisputed hero of the Star Wars trilogy, R2-D2. 143 | */ 144 | export function getHero(episode: number): CharacterSource { 145 | if (episode === 5) { 146 | // Luke is the hero of Episode V. 147 | return luke; 148 | } 149 | // Artoo is the hero otherwise. 150 | return artoo; 151 | } 152 | 153 | /** 154 | * Allows us to query for the human with the given id. 155 | */ 156 | export function getHuman(id: string): HumanSource | undefined { 157 | return humanData[id]; 158 | } 159 | 160 | /** 161 | * Allows us to query for the droid with the given id. 162 | */ 163 | export function getDroid(id: string): DroidSource | undefined { 164 | return droidData[id]; 165 | } -------------------------------------------------------------------------------- /tests/starwars/starWarsIntrospection.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow strict 8 | */ 9 | 10 | 11 | import { StarWarsSchema } from './starWarsSchema'; 12 | // @types/graphql has no def 13 | const graphqlSync = require('graphql').graphqlSync; 14 | 15 | 16 | describe('Star Wars Introspection Tests', () => { 17 | describe('Basic Introspection', () => { 18 | it('Allows querying the schema for types', () => { 19 | const query = ` 20 | query IntrospectionTypeQuery { 21 | __schema { 22 | types { 23 | name 24 | } 25 | } 26 | } 27 | `; 28 | const expected = { 29 | __schema: { 30 | types: [ 31 | { 32 | name: 'Human', 33 | }, 34 | { 35 | name: 'Character', 36 | }, 37 | { 38 | name: 'String', 39 | }, 40 | { 41 | name: 'Episode', 42 | }, 43 | { 44 | name: 'Droid', 45 | }, 46 | { 47 | name: 'Query', 48 | }, 49 | { 50 | name: 'Boolean', 51 | }, 52 | { 53 | name: '__Schema', 54 | }, 55 | { 56 | name: '__Type', 57 | }, 58 | { 59 | name: '__TypeKind', 60 | }, 61 | { 62 | name: '__Field', 63 | }, 64 | { 65 | name: '__InputValue', 66 | }, 67 | { 68 | name: '__EnumValue', 69 | }, 70 | { 71 | name: '__Directive', 72 | }, 73 | { 74 | name: '__DirectiveLocation', 75 | }, 76 | ], 77 | }, 78 | }; 79 | const result = graphqlSync(StarWarsSchema, query); 80 | expect(result).toEqual({ data: expected }); 81 | }); 82 | 83 | it('Allows querying the schema for query type', () => { 84 | const query = ` 85 | query IntrospectionQueryTypeQuery { 86 | __schema { 87 | queryType { 88 | name 89 | } 90 | } 91 | } 92 | `; 93 | const expected = { 94 | __schema: { 95 | queryType: { 96 | name: 'Query', 97 | }, 98 | }, 99 | }; 100 | const result = graphqlSync(StarWarsSchema, query); 101 | expect(result).toEqual({ data: expected }); 102 | }); 103 | 104 | it('Allows querying the schema for a specific type', () => { 105 | const query = ` 106 | query IntrospectionDroidTypeQuery { 107 | __type(name: "Droid") { 108 | name 109 | } 110 | } 111 | `; 112 | const expected = { 113 | __type: { 114 | name: 'Droid', 115 | }, 116 | }; 117 | const result = graphqlSync(StarWarsSchema, query); 118 | expect(result).toEqual({ data: expected }); 119 | }); 120 | 121 | it('Allows querying the schema for an object kind', () => { 122 | const query = ` 123 | query IntrospectionDroidKindQuery { 124 | __type(name: "Droid") { 125 | name 126 | kind 127 | } 128 | } 129 | `; 130 | const expected = { 131 | __type: { 132 | name: 'Droid', 133 | kind: 'OBJECT', 134 | }, 135 | }; 136 | const result = graphqlSync(StarWarsSchema, query); 137 | expect(result).toEqual({ data: expected }); 138 | }); 139 | 140 | it('Allows querying the schema for an interface kind', () => { 141 | const query = ` 142 | query IntrospectionCharacterKindQuery { 143 | __type(name: "Character") { 144 | name 145 | kind 146 | } 147 | } 148 | `; 149 | const expected = { 150 | __type: { 151 | name: 'Character', 152 | kind: 'INTERFACE', 153 | }, 154 | }; 155 | const result = graphqlSync(StarWarsSchema, query); 156 | expect(result).toEqual({ data: expected }); 157 | }); 158 | 159 | it('Allows querying the schema for object fields', () => { 160 | const query = ` 161 | query IntrospectionDroidFieldsQuery { 162 | __type(name: "Droid") { 163 | name 164 | fields { 165 | name 166 | type { 167 | name 168 | kind 169 | } 170 | } 171 | } 172 | } 173 | `; 174 | const expected = { 175 | __type: { 176 | name: 'Droid', 177 | fields: [ 178 | { 179 | name: 'id', 180 | type: { 181 | name: null, 182 | kind: 'NON_NULL', 183 | }, 184 | }, 185 | { 186 | name: 'name', 187 | type: { 188 | name: 'String', 189 | kind: 'SCALAR', 190 | }, 191 | }, 192 | { 193 | name: 'friends', 194 | type: { 195 | name: null, 196 | kind: 'LIST', 197 | }, 198 | }, 199 | { 200 | name: 'appearsIn', 201 | type: { 202 | name: null, 203 | kind: 'LIST', 204 | }, 205 | }, 206 | { 207 | name: 'secretBackstory', 208 | type: { 209 | name: 'String', 210 | kind: 'SCALAR', 211 | }, 212 | }, 213 | { 214 | name: 'primaryFunction', 215 | type: { 216 | name: 'String', 217 | kind: 'SCALAR', 218 | }, 219 | }, 220 | ], 221 | }, 222 | }; 223 | 224 | const result = graphqlSync(StarWarsSchema, query); 225 | expect(result).toEqual({ data: expected }); 226 | }); 227 | 228 | it('Allows querying the schema for nested object fields', () => { 229 | const query = ` 230 | query IntrospectionDroidNestedFieldsQuery { 231 | __type(name: "Droid") { 232 | name 233 | fields { 234 | name 235 | type { 236 | name 237 | kind 238 | ofType { 239 | name 240 | kind 241 | } 242 | } 243 | } 244 | } 245 | } 246 | `; 247 | const expected = { 248 | __type: { 249 | name: 'Droid', 250 | fields: [ 251 | { 252 | name: 'id', 253 | type: { 254 | name: null, 255 | kind: 'NON_NULL', 256 | ofType: { 257 | name: 'String', 258 | kind: 'SCALAR', 259 | }, 260 | }, 261 | }, 262 | { 263 | name: 'name', 264 | type: { 265 | name: 'String', 266 | kind: 'SCALAR', 267 | ofType: null, 268 | }, 269 | }, 270 | { 271 | name: 'friends', 272 | type: { 273 | name: null, 274 | kind: 'LIST', 275 | ofType: { 276 | name: 'Character', 277 | kind: 'INTERFACE', 278 | }, 279 | }, 280 | }, 281 | { 282 | name: 'appearsIn', 283 | type: { 284 | name: null, 285 | kind: 'LIST', 286 | ofType: { 287 | name: 'Episode', 288 | kind: 'ENUM', 289 | }, 290 | }, 291 | }, 292 | { 293 | name: 'secretBackstory', 294 | type: { 295 | name: 'String', 296 | kind: 'SCALAR', 297 | ofType: null, 298 | }, 299 | }, 300 | { 301 | name: 'primaryFunction', 302 | type: { 303 | name: 'String', 304 | kind: 'SCALAR', 305 | ofType: null, 306 | }, 307 | }, 308 | ], 309 | }, 310 | }; 311 | const result = graphqlSync(StarWarsSchema, query); 312 | expect(result).toEqual({ data: expected }); 313 | }); 314 | 315 | it('Allows querying the schema for field args', () => { 316 | const query = ` 317 | query IntrospectionQueryTypeQuery { 318 | __schema { 319 | queryType { 320 | fields { 321 | name 322 | args { 323 | name 324 | description 325 | type { 326 | name 327 | kind 328 | ofType { 329 | name 330 | kind 331 | } 332 | } 333 | defaultValue 334 | } 335 | } 336 | } 337 | } 338 | } 339 | `; 340 | const expected = { 341 | __schema: { 342 | queryType: { 343 | fields: [ 344 | { 345 | name: 'hero', 346 | args: [ 347 | { 348 | defaultValue: null, 349 | description: 350 | 'If omitted, returns the hero of the whole ' + 351 | 'saga. If provided, returns the hero of ' + 352 | 'that particular episode.', 353 | name: 'episode', 354 | type: { 355 | kind: 'ENUM', 356 | name: 'Episode', 357 | ofType: null, 358 | }, 359 | }, 360 | ], 361 | }, 362 | { 363 | name: 'human', 364 | args: [ 365 | { 366 | name: 'id', 367 | description: 'id of the human', 368 | type: { 369 | kind: 'NON_NULL', 370 | name: null, 371 | ofType: { 372 | kind: 'SCALAR', 373 | name: 'String', 374 | }, 375 | }, 376 | defaultValue: null, 377 | }, 378 | ], 379 | }, 380 | { 381 | name: 'droid', 382 | args: [ 383 | { 384 | name: 'id', 385 | description: 'id of the droid', 386 | type: { 387 | kind: 'NON_NULL', 388 | name: null, 389 | ofType: { 390 | kind: 'SCALAR', 391 | name: 'String', 392 | }, 393 | }, 394 | defaultValue: null, 395 | }, 396 | ], 397 | }, 398 | ], 399 | }, 400 | }, 401 | }; 402 | 403 | const result = graphqlSync(StarWarsSchema, query); 404 | expect(result).toEqual({ data: expected }); 405 | }); 406 | 407 | it('Allows querying the schema for documentation', () => { 408 | const query = ` 409 | query IntrospectionDroidDescriptionQuery { 410 | __type(name: "Droid") { 411 | name 412 | description 413 | } 414 | } 415 | `; 416 | const expected = { 417 | __type: { 418 | name: 'Droid', 419 | description: 'A mechanical creature in the Star Wars universe.', 420 | }, 421 | }; 422 | const result = graphqlSync(StarWarsSchema, query); 423 | expect(result).toEqual({ data: expected }); 424 | }); 425 | }); 426 | }); -------------------------------------------------------------------------------- /tests/starwars/starWarsQuery.test.ts: -------------------------------------------------------------------------------- 1 | import { StarWarsSchema } from './starWarsSchema'; 2 | import { graphql } from 'graphql'; 3 | 4 | 5 | describe('Star Wars Query Tests', () => { 6 | describe('Basic Queries', () => { 7 | it('Correctly identifies R2-D2 as the hero of the Star Wars Saga', async () => { 8 | const query = ` 9 | query HeroNameQuery { 10 | hero { 11 | name 12 | } 13 | } 14 | `; 15 | const result = await graphql(StarWarsSchema, query); 16 | expect(result).toEqual({ 17 | data: { 18 | hero: { 19 | name: 'R2-D2', 20 | }, 21 | }, 22 | }); 23 | }); 24 | 25 | it('Accepts an object with named properties to graphql()', async () => { 26 | const query = ` 27 | query HeroNameQuery { 28 | hero { 29 | name 30 | } 31 | } 32 | `; 33 | const result = await graphql({ 34 | schema: StarWarsSchema, 35 | source: query, 36 | }); 37 | expect(result).toEqual({ 38 | data: { 39 | hero: { 40 | name: 'R2-D2', 41 | }, 42 | }, 43 | }); 44 | }); 45 | 46 | it('Allows us to query for the ID and friends of R2-D2', async () => { 47 | const query = ` 48 | query HeroNameAndFriendsQuery { 49 | hero { 50 | id 51 | name 52 | friends { 53 | name 54 | } 55 | } 56 | } 57 | `; 58 | const result = await graphql(StarWarsSchema, query); 59 | expect(result).toEqual({ 60 | data: { 61 | hero: { 62 | id: '2001', 63 | name: 'R2-D2', 64 | friends: [ 65 | { 66 | name: 'Luke Skywalker', 67 | }, 68 | { 69 | name: 'Han Solo', 70 | }, 71 | { 72 | name: 'Leia Organa', 73 | }, 74 | ], 75 | }, 76 | }, 77 | }); 78 | }); 79 | }); 80 | 81 | describe('Nested Queries', () => { 82 | it('Allows us to query for the friends of friends of R2-D2', async () => { 83 | const query = ` 84 | query NestedQuery { 85 | hero { 86 | name 87 | friends { 88 | name 89 | appearsIn 90 | friends { 91 | name 92 | } 93 | } 94 | } 95 | } 96 | `; 97 | const result = await graphql(StarWarsSchema, query); 98 | expect(result).toEqual({ 99 | data: { 100 | hero: { 101 | name: 'R2-D2', 102 | friends: [ 103 | { 104 | name: 'Luke Skywalker', 105 | appearsIn: ['NEWHOPE', 'EMPIRE', 'JEDI'], 106 | friends: [ 107 | { 108 | name: 'Han Solo', 109 | }, 110 | { 111 | name: 'Leia Organa', 112 | }, 113 | { 114 | name: 'C-3PO', 115 | }, 116 | { 117 | name: 'R2-D2', 118 | }, 119 | ], 120 | }, 121 | { 122 | name: 'Han Solo', 123 | appearsIn: ['NEWHOPE', 'EMPIRE', 'JEDI'], 124 | friends: [ 125 | { 126 | name: 'Luke Skywalker', 127 | }, 128 | { 129 | name: 'Leia Organa', 130 | }, 131 | { 132 | name: 'R2-D2', 133 | }, 134 | ], 135 | }, 136 | { 137 | name: 'Leia Organa', 138 | appearsIn: ['NEWHOPE', 'EMPIRE', 'JEDI'], 139 | friends: [ 140 | { 141 | name: 'Luke Skywalker', 142 | }, 143 | { 144 | name: 'Han Solo', 145 | }, 146 | { 147 | name: 'C-3PO', 148 | }, 149 | { 150 | name: 'R2-D2', 151 | }, 152 | ], 153 | }, 154 | ], 155 | }, 156 | }, 157 | }); 158 | }); 159 | }); 160 | 161 | describe('Using IDs and query parameters to refetch objects', () => { 162 | it('Allows us to query for Luke Skywalker directly, using his ID', async () => { 163 | const query = ` 164 | query FetchLukeQuery { 165 | human(id: "1000") { 166 | name 167 | } 168 | } 169 | `; 170 | const result = await graphql(StarWarsSchema, query); 171 | expect(result).toEqual({ 172 | data: { 173 | human: { 174 | name: 'Luke Skywalker', 175 | }, 176 | }, 177 | }); 178 | }); 179 | 180 | it('Allows us to create a generic query, then use it to fetch Luke Skywalker using his ID', async () => { 181 | const query = ` 182 | query FetchSomeIDQuery($someId: String!) { 183 | human(id: $someId) { 184 | name 185 | } 186 | } 187 | `; 188 | const params = { someId: '1000' }; 189 | const result = await graphql(StarWarsSchema, query, null, null, params); 190 | expect(result).toEqual({ 191 | data: { 192 | human: { 193 | name: 'Luke Skywalker', 194 | }, 195 | }, 196 | }); 197 | }); 198 | 199 | it('Allows us to create a generic query, then use it to fetch Han Solo using his ID', async () => { 200 | const query = ` 201 | query FetchSomeIDQuery($someId: String!) { 202 | human(id: $someId) { 203 | name 204 | } 205 | } 206 | `; 207 | const params = { someId: '1002' }; 208 | const result = await graphql(StarWarsSchema, query, null, null, params); 209 | expect(result).toEqual({ 210 | data: { 211 | human: { 212 | name: 'Han Solo', 213 | }, 214 | }, 215 | }); 216 | }); 217 | 218 | it('Allows us to create a generic query, then pass an invalid ID to get null back', async () => { 219 | const query = ` 220 | query humanQuery($id: String!) { 221 | human(id: $id) { 222 | name 223 | } 224 | } 225 | `; 226 | const params = { id: 'not a valid id' }; 227 | const result = await graphql(StarWarsSchema, query, null, null, params); 228 | expect(result).toEqual({ 229 | data: { 230 | human: null, 231 | }, 232 | }); 233 | }); 234 | }); 235 | 236 | describe('Using aliases to change the key in the response', () => { 237 | it('Allows us to query for Luke, changing his key with an alias', async () => { 238 | const query = ` 239 | query FetchLukeAliased { 240 | luke: human(id: "1000") { 241 | name 242 | } 243 | } 244 | `; 245 | const result = await graphql(StarWarsSchema, query); 246 | expect(result).toEqual({ 247 | data: { 248 | luke: { 249 | name: 'Luke Skywalker', 250 | }, 251 | }, 252 | }); 253 | }); 254 | 255 | it('Allows us to query for both Luke and Leia, using two root fields and an alias', async () => { 256 | const query = ` 257 | query FetchLukeAndLeiaAliased { 258 | luke: human(id: "1000") { 259 | name 260 | } 261 | leia: human(id: "1003") { 262 | name 263 | } 264 | } 265 | `; 266 | const result = await graphql(StarWarsSchema, query); 267 | expect(result).toEqual({ 268 | data: { 269 | luke: { 270 | name: 'Luke Skywalker', 271 | }, 272 | leia: { 273 | name: 'Leia Organa', 274 | }, 275 | }, 276 | }); 277 | }); 278 | }); 279 | 280 | describe('Uses fragments to express more complex queries', () => { 281 | it('Allows us to query using duplicated content', async () => { 282 | const query = ` 283 | query DuplicateFields { 284 | luke: human(id: "1000") { 285 | name 286 | homePlanet 287 | } 288 | leia: human(id: "1003") { 289 | name 290 | homePlanet 291 | } 292 | } 293 | `; 294 | const result = await graphql(StarWarsSchema, query); 295 | expect(result).toEqual({ 296 | data: { 297 | luke: { 298 | name: 'Luke Skywalker', 299 | homePlanet: 'Tatooine', 300 | }, 301 | leia: { 302 | name: 'Leia Organa', 303 | homePlanet: 'Alderaan', 304 | }, 305 | }, 306 | }); 307 | }); 308 | 309 | it('Allows us to use a fragment to avoid duplicating content', async () => { 310 | const query = ` 311 | query UseFragment { 312 | luke: human(id: "1000") { 313 | ...HumanFragment 314 | } 315 | leia: human(id: "1003") { 316 | ...HumanFragment 317 | } 318 | } 319 | fragment HumanFragment on Human { 320 | name 321 | homePlanet 322 | } 323 | `; 324 | const result = await graphql(StarWarsSchema, query); 325 | expect(result).toEqual({ 326 | data: { 327 | luke: { 328 | name: 'Luke Skywalker', 329 | homePlanet: 'Tatooine', 330 | }, 331 | leia: { 332 | name: 'Leia Organa', 333 | homePlanet: 'Alderaan', 334 | }, 335 | }, 336 | }); 337 | }); 338 | }); 339 | 340 | describe('Using __typename to find the type of an object', () => { 341 | it('Allows us to verify that R2-D2 is a droid', async () => { 342 | const query = ` 343 | query CheckTypeOfR2 { 344 | hero { 345 | __typename 346 | name 347 | } 348 | } 349 | `; 350 | const result = await graphql(StarWarsSchema, query); 351 | expect(result).toEqual({ 352 | data: { 353 | hero: { 354 | __typename: 'Droid', 355 | name: 'R2-D2', 356 | }, 357 | }, 358 | }); 359 | }); 360 | 361 | it('Allows us to verify that Luke is a human', async () => { 362 | const query = ` 363 | query CheckTypeOfLuke { 364 | hero(episode: EMPIRE) { 365 | __typename 366 | name 367 | } 368 | } 369 | `; 370 | const result = await graphql(StarWarsSchema, query); 371 | expect(result).toEqual({ 372 | data: { 373 | hero: { 374 | __typename: 'Human', 375 | name: 'Luke Skywalker', 376 | }, 377 | }, 378 | }); 379 | }); 380 | }); 381 | 382 | describe('Reporting errors raised in resolvers', () => { 383 | it('Correctly reports error on accessing secretBackstory', async () => { 384 | const query = ` 385 | query HeroNameQuery { 386 | hero { 387 | name 388 | secretBackstory 389 | } 390 | } 391 | `; 392 | const result = await graphql(StarWarsSchema, query); 393 | expect(result).toEqual({ 394 | data: { 395 | hero: { 396 | name: 'R2-D2', 397 | secretBackstory: null, 398 | }, 399 | }, 400 | errors: [ 401 | { 402 | message: 'secretBackstory is secret.', 403 | locations: [{ line: 5, column: 13 }], 404 | path: ['hero', 'secretBackstory'], 405 | }, 406 | ], 407 | }); 408 | }); 409 | 410 | it('Correctly reports error on accessing secretBackstory in a list', async () => { 411 | const query = ` 412 | query HeroNameQuery { 413 | hero { 414 | name 415 | friends { 416 | name 417 | secretBackstory 418 | } 419 | } 420 | } 421 | `; 422 | const result = await graphql(StarWarsSchema, query); 423 | expect(result).toEqual({ 424 | data: { 425 | hero: { 426 | name: 'R2-D2', 427 | friends: [ 428 | { 429 | name: 'Luke Skywalker', 430 | secretBackstory: null, 431 | }, 432 | { 433 | name: 'Han Solo', 434 | secretBackstory: null, 435 | }, 436 | { 437 | name: 'Leia Organa', 438 | secretBackstory: null, 439 | }, 440 | ], 441 | }, 442 | }, 443 | errors: [ 444 | { 445 | message: 'secretBackstory is secret.', 446 | locations: [{ line: 7, column: 15 }], 447 | path: ['hero', 'friends', 0, 'secretBackstory'], 448 | }, 449 | { 450 | message: 'secretBackstory is secret.', 451 | locations: [{ line: 7, column: 15 }], 452 | path: ['hero', 'friends', 1, 'secretBackstory'], 453 | }, 454 | { 455 | message: 'secretBackstory is secret.', 456 | locations: [{ line: 7, column: 15 }], 457 | path: ['hero', 'friends', 2, 'secretBackstory'], 458 | }, 459 | ], 460 | }); 461 | }); 462 | 463 | it('Correctly reports error on accessing through an alias', async () => { 464 | const query = ` 465 | query HeroNameQuery { 466 | mainHero: hero { 467 | name 468 | story: secretBackstory 469 | } 470 | } 471 | `; 472 | const result = await graphql(StarWarsSchema, query); 473 | expect(result).toEqual({ 474 | data: { 475 | mainHero: { 476 | name: 'R2-D2', 477 | story: null, 478 | }, 479 | }, 480 | errors: [ 481 | { 482 | message: 'secretBackstory is secret.', 483 | locations: [{ line: 5, column: 13 }], 484 | path: ['mainHero', 'story'], 485 | }, 486 | ], 487 | }); 488 | }); 489 | }); 490 | }); -------------------------------------------------------------------------------- /tests/starwars/starWarsSchema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLEnumType, GraphQLSchema } from 'graphql'; 2 | 3 | import { getFriends, getHero, getHuman, getDroid, EpisodeValue, CharacterSource, HumanSource, DroidSource } from './starWarsData'; 4 | 5 | import { getType, defineType, gql } from '../../src'; 6 | import { ResolverContext, source } from '../../src/utilities/ResolverContext'; 7 | 8 | 9 | const episodeEnum = new GraphQLEnumType({ 10 | name: 'Episode', 11 | description: 'One of the films in the Star Wars Trilogy', 12 | values: { 13 | NEWHOPE: { 14 | value: 4, 15 | description: 'Released in 1977.', 16 | }, 17 | EMPIRE: { 18 | value: 5, 19 | description: 'Released in 1980.', 20 | }, 21 | JEDI: { 22 | value: 6, 23 | description: 'Released in 1983.', 24 | }, 25 | }, 26 | }); 27 | 28 | 29 | @defineType(gql` 30 | """ 31 | A character in the Star Wars Trilogy 32 | """ 33 | interface Character { 34 | """ 35 | The id of the character. 36 | """ 37 | id: String! 38 | 39 | """ 40 | the name of the character. 41 | """ 42 | name: String 43 | 44 | """ 45 | The friends of the character, or an empty list if they have none. 46 | """ 47 | friends: [${Character}] 48 | 49 | """ 50 | Which movies they appear in. 51 | """ 52 | appearsIn: [${episodeEnum}] 53 | 54 | """ 55 | All secrets about their past. 56 | """ 57 | secretBackstory: String 58 | } 59 | `) 60 | abstract class Character extends ResolverContext { 61 | @source() id: string; 62 | @source() name: string; 63 | @source() appearsIn: EpisodeValue[]; 64 | 65 | get secretBackstory(): string { 66 | throw new Error('secretBackstory is secret.'); 67 | } 68 | 69 | async friends() { 70 | const friendSources = await Promise.all(getFriends(this.$source)); 71 | if (friendSources) { 72 | return friendSources.map(Character.$fromSource); 73 | } 74 | return null; 75 | } 76 | 77 | static $fromSource = (source: CharacterSource): Human | Droid | null => { 78 | if (source.type === 'Human') { 79 | return new Human(source as HumanSource); 80 | } else if (source.type === 'Droid') { 81 | return new Droid(source as DroidSource); 82 | } else { 83 | return null; 84 | } 85 | } 86 | } 87 | 88 | 89 | @defineType(gql` 90 | """ 91 | A humanoid creature in the Star Wars universe. 92 | """ 93 | type Human implements ${Character} { 94 | """ 95 | The home planet of the human, or null if unknown. 96 | """ 97 | homePlanet: String 98 | 99 | # and fields from class Character 100 | } 101 | `) 102 | class Human extends Character { 103 | @source() homePlanet?: string; 104 | } 105 | 106 | 107 | @defineType(gql` 108 | """ 109 | A mechanical creature in the Star Wars universe. 110 | """ 111 | type Droid implements ${Character} { 112 | 113 | """ 114 | The primary function of the droid. 115 | """ 116 | primaryFunction: String 117 | 118 | # and fields from class Character 119 | } 120 | `) 121 | class Droid extends Character { 122 | @source() primaryFunction: string; 123 | } 124 | 125 | 126 | @defineType(gql` 127 | type Query { 128 | hero( 129 | """ 130 | If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode. 131 | """ 132 | episode: ${episodeEnum} 133 | ): ${Character} 134 | 135 | human( 136 | """ 137 | id of the human 138 | """ 139 | id: String! 140 | ): ${Human} 141 | 142 | droid( 143 | """ 144 | id of the droid 145 | """ 146 | id: String! 147 | ): ${Droid} 148 | } 149 | `) 150 | class Query { 151 | public static hero(_source: null, { episode }: { episode: number }) { 152 | return Character.$fromSource(getHero(episode)); 153 | } 154 | public static human(_source: null, { id }: { id: string }) { 155 | const source = getHuman(id); 156 | return source && new Human(source); 157 | } 158 | public static droid(_source: null, { id }: { id: string }) { 159 | const source = getDroid(id); 160 | return source && new Droid(source); 161 | } 162 | } 163 | 164 | export const StarWarsSchema = new GraphQLSchema({ 165 | query: getType(Query), 166 | types: [ 167 | getType(Human), 168 | getType(Droid), 169 | ] 170 | }); 171 | -------------------------------------------------------------------------------- /tests/starwars/starWarsValidation.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { StarWarsSchema } from './starWarsSchema'; 3 | import { Source, parse, validate } from 'graphql'; 4 | 5 | /** 6 | * Helper function to test a query and the expected response. 7 | */ 8 | function validationErrors(query: string) { 9 | const source = new Source(query, 'StarWars.graphql'); 10 | const ast = parse(source); 11 | return validate(StarWarsSchema, ast); 12 | } 13 | 14 | describe('Star Wars Validation Tests', () => { 15 | describe('Basic Queries', () => { 16 | it('Validates a complex but valid query', () => { 17 | const query = ` 18 | query NestedQueryWithFragment { 19 | hero { 20 | ...NameAndAppearances 21 | friends { 22 | ...NameAndAppearances 23 | friends { 24 | ...NameAndAppearances 25 | } 26 | } 27 | } 28 | } 29 | fragment NameAndAppearances on Character { 30 | name 31 | appearsIn 32 | } 33 | `; 34 | return expect(validationErrors(query)).toHaveLength(0); 35 | }); 36 | 37 | it('Notes that non-existent fields are invalid', () => { 38 | const query = ` 39 | query HeroSpaceshipQuery { 40 | hero { 41 | favoriteSpaceship 42 | } 43 | } 44 | `; 45 | return expect(validationErrors(query)).not.toHaveLength(0); 46 | }); 47 | 48 | it('Requires fields on objects', () => { 49 | const query = ` 50 | query HeroNoFieldsQuery { 51 | hero 52 | } 53 | `; 54 | return expect(validationErrors(query)).not.toHaveLength(0); 55 | }); 56 | 57 | it('Disallows fields on scalars', () => { 58 | const query = ` 59 | query HeroFieldsOnScalarQuery { 60 | hero { 61 | name { 62 | firstCharacterOfName 63 | } 64 | } 65 | } 66 | `; 67 | return expect(validationErrors(query)).not.toHaveLength(0); 68 | }); 69 | 70 | it('Disallows object fields on interfaces', () => { 71 | const query = ` 72 | query DroidFieldOnCharacter { 73 | hero { 74 | name 75 | primaryFunction 76 | } 77 | } 78 | `; 79 | return expect(validationErrors(query)).not.toHaveLength(0); 80 | }); 81 | 82 | it('Allows object fields in fragments', () => { 83 | const query = ` 84 | query DroidFieldInFragment { 85 | hero { 86 | name 87 | ...DroidFields 88 | } 89 | } 90 | fragment DroidFields on Droid { 91 | primaryFunction 92 | } 93 | `; 94 | return expect(validationErrors(query)).toHaveLength(0); 95 | }); 96 | 97 | it('Allows object fields in inline fragments', () => { 98 | const query = ` 99 | query DroidFieldInFragment { 100 | hero { 101 | name 102 | ... on Droid { 103 | primaryFunction 104 | } 105 | } 106 | } 107 | `; 108 | return expect(validationErrors(query)).toHaveLength(0); 109 | }); 110 | }); 111 | }); -------------------------------------------------------------------------------- /tests/typeutils.test.ts: -------------------------------------------------------------------------------- 1 | import { equalsOrInherits } from '../src'; 2 | 3 | 4 | test('isSubClassOf', () => { 5 | 6 | class Foo {} 7 | class Bar extends Foo {} 8 | class Baz extends Bar {} 9 | 10 | expect(equalsOrInherits(Foo, Foo)).toBe(true); 11 | expect(equalsOrInherits(Bar, Foo)).toBe(true); 12 | expect(equalsOrInherits(Baz, Foo)).toBe(true); 13 | expect(equalsOrInherits(Baz, Bar)).toBe(true); 14 | expect(equalsOrInherits(Foo, Bar)).toBe(false); 15 | expect(equalsOrInherits(Foo, Baz)).toBe(false); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/utilities.test.ts: -------------------------------------------------------------------------------- 1 | import { formatObjectInfo } from '../src/utilities/formatObjectInfo'; 2 | 3 | 4 | test('formatObjectInfo', () => { 5 | class Foo {} 6 | 7 | expect(formatObjectInfo(Foo)).toBe('Foo'); 8 | expect(formatObjectInfo(new Foo())).toBe('[object Object]'); 9 | expect(formatObjectInfo(3)).toBe('3'); 10 | expect(formatObjectInfo(null)).toBe('null'); 11 | expect(formatObjectInfo(undefined)).toBe('undefined'); 12 | expect(formatObjectInfo('STR')).toBe('STR'); 13 | expect(formatObjectInfo({})).toBe('[object Object]'); 14 | expect(formatObjectInfo(Object)).toBe('Object'); 15 | expect(formatObjectInfo((() => {}))).toBe('[anonymous function]'); 16 | }); 17 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitThis": true, 4 | "preserveConstEnums": true, 5 | "lib": [ 6 | "es5", 7 | "es6", 8 | "esnext.asynciterable" 9 | ], 10 | "target": "es2016", 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "allowJs": false, 14 | "esModuleInterop": true, 15 | "emitDecoratorMetadata": true, 16 | "experimentalDecorators": true, 17 | "downlevelIteration": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "sourceMap": true, 20 | "declarationMap": true, 21 | "noImplicitAny": true, 22 | "declaration": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noImplicitReturns": true, 25 | "stripInternal": true, 26 | "pretty": true, 27 | "strictNullChecks": true, 28 | "noUnusedLocals": true 29 | } 30 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "rootDir": "./src", 6 | "composite": true 7 | }, 8 | "include": [ 9 | "src/**/*.ts" 10 | ], 11 | "exclude": [ 12 | "tests", 13 | "lib" 14 | ] 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | } 6 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "rules": { 4 | "class-name": true, 5 | "comment-format": [ 6 | true, 7 | "check-space" 8 | ], 9 | "indent": [ 10 | true, 11 | "spaces" 12 | ], 13 | "no-duplicate-variable": true, 14 | "no-eval": true, 15 | "no-internal-module": true, 16 | "no-var-keyword": true, 17 | "one-line": [ 18 | true, 19 | "check-open-brace", 20 | "check-whitespace" 21 | ], 22 | "quotemark": [ 23 | true, 24 | "single" 25 | ], 26 | "semicolon": true, 27 | "triple-equals": [ 28 | true, 29 | "allow-null-check" 30 | ], 31 | "typedef-whitespace": [ 32 | true, 33 | { 34 | "call-signature": "nospace", 35 | "index-signature": "nospace", 36 | "parameter": "nospace", 37 | "property-declaration": "nospace", 38 | "variable-declaration": "nospace" 39 | } 40 | ], 41 | "variable-name": [ 42 | true, 43 | "ban-keywords" 44 | ], 45 | "whitespace": [ 46 | true, 47 | "check-branch", 48 | "check-decl", 49 | "check-operator", 50 | "check-separator", 51 | "check-type" 52 | ] 53 | } 54 | } --------------------------------------------------------------------------------