Repository URL to install this package:
|
Version:
0.0.6 ▾
|
// tslint:disable
import 'reflect-metadata'
import {
CommunicationBridge,
EventEmitterCommunicationBridge,
ModuleConfig,
ModuleContext,
OnRequest,
ModuleConfigRequiredError,
} from '@graphql-modules/core'
import {
execute,
GraphQLSchema,
printSchema,
GraphQLString,
defaultFieldResolver,
print,
GraphQLScalarType,
Kind,
} from 'graphql'
import gql from 'graphql-tag'
import { SchemaDirectiveVisitor, makeExecutableSchema } from 'graphql-tools'
import { ModuleSessionInfo } from '@graphql-modules/core/dist/module-session-info'
import {
Injectable,
Inject,
Injector,
ProviderScope,
DependencyProviderNotFoundError,
} from '@graphql-modules/di'
import { GraphQLModule } from '../'
function stripWhiteSpaces(str: string): string {
return str.replace(/\s+/g, ' ').trim()
}
describe('GraphQLModule', () => {
// A
@Injectable()
class ProviderA {
doSomething() {
return 'Test1'
}
}
// B
@Injectable()
class ProviderB {
doSomethingElse() {
return 'Test2'
}
}
const typesA = [`type A { f: String}`, `type Query { a: A }`]
const moduleA = new GraphQLModule({
name: 'A',
typeDefs: typesA,
resolvers: ({ injector }) => ({
Query: { a: () => ({}) },
A: { f: () => injector.get(ProviderA).doSomething() },
}),
providers: [ProviderA],
})
// B
const typesB = [`type B { f: String}`, `type Query { b: B }`]
const resolversB = {
Query: { b: () => ({}) },
B: { f: (root, args, context) => context.user.id },
}
let resolverCompositionCalled = false
const moduleB = new GraphQLModule({
name: 'B',
typeDefs: typesB,
resolvers: resolversB,
resolversComposition: {
'B.f': next => async (root, args, context: ModuleContext, info) => {
if (context.injector && context.injector.get(ModuleConfig(moduleB))) {
resolverCompositionCalled = true
}
return next(root, args, context, info)
},
},
imports: () => [moduleC],
})
// C (with context building fn)
const cContextBuilder = () => ({ user: { id: 1 } })
const typesC = [`type C { f: String}`, `type Query { c: C }`]
const moduleC = new GraphQLModule({
name: 'C',
typeDefs: typesC,
context: cContextBuilder,
})
// D
const moduleD = new GraphQLModule({
name: 'D',
typeDefs: typesC,
context: () => {
throw new Error('oops')
},
})
// E
const moduleE = new GraphQLModule({
name: 'E',
typeDefs: typesC,
})
// F
const typeDefsFnMock = jest.fn().mockReturnValue(typesC)
const resolversFnMock = jest.fn().mockReturnValue({ C: {} })
const moduleF = new GraphQLModule({
name: 'F',
typeDefs: typeDefsFnMock,
resolvers: resolversFnMock,
})
afterEach(() => {
typeDefsFnMock.mockClear()
resolversFnMock.mockClear()
})
// Queries
const testQuery = gql`
query {
b {
f
}
}
`
const app = new GraphQLModule({
imports: [moduleA, moduleB.forRoot({}), moduleC],
})
it('should return the correct GraphQLSchema', async () => {
const schema = app.schema
expect(schema).toBeDefined()
expect((schema as any) instanceof GraphQLSchema).toBeTruthy()
expect(stripWhiteSpaces(printSchema(schema))).toBe(
stripWhiteSpaces(`
type A {
f: String
}
type B {
f: String
}
type C {
f: String
}
type Query {
a: A
c: C
b: B
}`)
)
})
it('should trigger the correct GraphQL context builders and build the correct context', async () => {
const schema = app.schema
const context = await app.context({ req: {} })
const result = await execute({
schema,
document: testQuery,
contextValue: context,
})
expect(result.errors).toBeFalsy()
expect(result.data.b.f).toBe('1')
})
it('should work without a GraphQL schema and set providers', async () => {
const provider = {}
const token = Symbol.for('provider')
const module = new GraphQLModule({
providers: [
{
provide: token,
useValue: provider,
},
],
})
const { injector } = new GraphQLModule({ imports: [module] })
expect(injector.get(token)).toBe(provider)
})
it('should put the correct providers to the injector', async () => {
expect(app.injector.get(ProviderA) instanceof ProviderA).toBe(true)
})
it('should allow to get schema', async () => {
expect(app.schema).toBeDefined()
})
it('should inject dependencies to factory functions using Inject', async () => {
const { schema, context } = new GraphQLModule({
typeDefs: gql`
type Query {
something: String
somethingElse: String
}
`,
providers: [ProviderA, ProviderB],
resolvers: Inject(ProviderA, ProviderB)((providerA, providerB) => ({
Query: {
something: () => providerA.doSomething(),
somethingElse: () => providerB.doSomethingElse(),
},
})),
})
const contextValue = await context({ req: {} })
const result = await execute({
schema,
document: gql`
query {
something
somethingElse
}
`,
contextValue,
})
expect(result.errors).toBeFalsy()
expect(result.data.something).toBe('Test1')
expect(result.data.somethingElse).toBe('Test2')
})
describe('Schema merging', () => {
it('should merge types and directives correctly', async () => {
const m1 = new GraphQLModule({
typeDefs: [
`directive @entity on OBJECT`,
`directive @field on FIELD_DEFINITION`,
`type A @entity { f: String }`,
`type Query { a: [A!] }`,
],
})
const m2 = new GraphQLModule({
typeDefs: [
`directive @entity on OBJECT`,
`directive @field on FIELD_DEFINITION`,
`type A @entity { f: String @field }`,
`type Query { a: [A!] }`,
],
})
const app = new GraphQLModule({
imports: [m1, m2],
})
const aFields = (app.schema.getTypeMap().A as any).getFields()
const node = aFields.f.astNode
expect(node.directives.length).toBe(1)
})
})
describe('Module Dependencies', () => {
it('should init modules in the right order', async () => {
let counter = 0
@Injectable()
class Provider1 {
count: number
constructor() {
this.count = counter++
}
}
@Injectable()
class Provider2 {
count: number
constructor() {
this.count = counter++
}
}
const module1 = new GraphQLModule({
imports: () => [module2],
providers: [Provider1],
})
const module2 = new GraphQLModule({ providers: [Provider2] })
const { injector } = new GraphQLModule({ imports: [module2, module1] })
expect(injector.get(Provider1).count).toEqual(1)
expect(injector.get(Provider2).count).toEqual(0)
expect(counter).toEqual(2)
})
it('should init modules in the right order with multiple circular dependencies', async () => {
let counter = 0
@Injectable()
class Provider1 {
count: number
constructor() {
this.count = counter++
}
}
@Injectable()
class Provider2 {
count: number
constructor() {
this.count = counter++
}
}
@Injectable()
class Provider3 {
count: number
constructor() {
this.count = counter++
}
}
const module1 = new GraphQLModule({
imports: () => [module2],
providers: [Provider1],
})
const module2 = new GraphQLModule({
imports: () => [module1],
providers: [Provider2],
})
const module3 = new GraphQLModule({
imports: () => [module1],
providers: [Provider3],
})
const { injector } = new GraphQLModule({
imports: [module2, module1, module3],
})
expect(injector.get(Provider1).count).toEqual(1)
expect(injector.get(Provider2).count).toEqual(0)
expect(counter).toEqual(3)
})
it('should init modules in the right order with 2 circular dependencies', async () => {
let counter = 0
@Injectable()
class Provider1 {
count: number
constructor() {
this.count = counter++
}
}
@Injectable()
class Provider2 {
count: number
constructor() {
this.count = counter++
}
}
const module1 = new GraphQLModule({
imports: () => [module2],
providers: [Provider1],
})
const module2 = new GraphQLModule({
imports: () => [module1],
providers: [Provider2],
})
const { injector } = new GraphQLModule({ imports: [module2, module1] })
expect(injector.get(Provider1).count).toEqual(1)
expect(injector.get(Provider2).count).toEqual(0)
expect(counter).toEqual(2)
})
it('should set config per each module', async () => {
interface IModuleConfig {
test: number
}
const module1 = new GraphQLModule({
imports: () => [module2],
providers: () => [Provider1],
}).forRoot({ test: 1 })
const module2 = new GraphQLModule({
providers: () => [Provider2],
}).forRoot({ test: 2 })
@Injectable()
class Provider1 {
test: number
constructor(@Inject(ModuleConfig(module1)) config: IModuleConfig) {
this.test = config.test
}
}
@Injectable()
class Provider2 {
test: number
constructor(@Inject(ModuleConfig(module2)) config: IModuleConfig) {
this.test = config.test
}
}
const { injector } = new GraphQLModule({ imports: [module2, module1] })
expect(injector.get(Provider1).test).toEqual(1)
expect(injector.get(Provider2).test).toEqual(2)
})
it('should not allow to use modules without configuration if required', async () => {
let error
try {
const { injector } = new GraphQLModule({
configRequired: true,
})
} catch (e) {
error = e
}
expect(error).toBeInstanceOf(ModuleConfigRequiredError)
})
it('should encapsulate between providers from different non-dependent modules', async () => {
class ProviderA {
test = 0
}
const moduleB = new GraphQLModule({ providers: [ProviderA] })
@Injectable()
class ProviderB {
constructor(providerA: ProviderA) {}
}
const moduleA = new GraphQLModule({ providers: [ProviderB] })
try {
const { injector } = new GraphQLModule({ imports: [moduleA, moduleB] })
injector.get(ProviderB)
} catch (e) {
expect(e instanceof DependencyProviderNotFoundError).toBeTruthy()
expect(e.dependent === ProviderB).toBeTruthy()
expect(e.dependency === ProviderA).toBeTruthy()
}
})
it('should encapsulate resolvers', async () => {
@Injectable()
class ProviderB {
test = 1
}
try {
const moduleA = new GraphQLModule({
typeDefs: gql`
type Query {
test: String
}
`,
resolvers: Inject(ProviderB)(providerB => ({
Query: {
test: () => providerB.test,
},
})),
})
const moduleB = new GraphQLModule({ providers: [ProviderB] })
const { schema, context } = new GraphQLModule({
imports: [moduleA, moduleB],
})
const contextValue = await context({ req: {} })
const result = await execute({
schema,
document: gql`
query {
test
}
`,
contextValue,
})
} catch (e) {
expect(e.message).toContain('ProviderB not provided in')
}
})
it('should throw error if mergeCircularImports is disabled', async () => {
const moduleA = new GraphQLModule({
imports: () => [moduleA],
})
const moduleB = new GraphQLModule({
imports: () => [moduleB],
})
let errorMsg
try {
const { schema, context } = new GraphQLModule({
imports: [moduleA, moduleB],
mergeCircularImports: false,
})
} catch (e) {
errorMsg = e.message
}
expect(errorMsg).toContain('Dependency Cycle')
})
})
describe('CommunicationBridge', async () => {
it('should set CommunicationBridge correctly', async () => {
const communicationBridge = new EventEmitterCommunicationBridge()
const { injector } = new GraphQLModule({
providers: [
{
provide: CommunicationBridge,
useValue: communicationBridge,
},
],
})
expect(
injector.get(CommunicationBridge) === communicationBridge
).toBeTruthy()
})
})
describe('onRequest Hook', async () => {
it('should call onRequest hook on each request', async () => {
let counter = 0
@Injectable()
class FooProvider implements OnRequest {
onRequest() {
counter++
}
}
const { schema, context } = new GraphQLModule({
typeDefs: gql`
type Query {
foo: String
}
`,
resolvers: {
Query: {
foo: () => '',
},
},
providers: [FooProvider],
})
await execute({
schema,
document: gql`
query {
foo
}
`,
contextValue: await context({}),
})
expect(counter).toBe(1)
await execute({
schema,
document: gql`
query {
foo
}
`,
contextValue: await context({}),
})
expect(counter).toBe(2)
await execute({
schema,
document: gql`
query {
foo
}
`,
contextValue: await context({}),
})
expect(counter).toBe(3)
})
it('should pass network request to onRequest hook', async () => {
const fooRequest = {
foo: 'bar',
}
let receivedRequest
@Injectable()
class FooProvider implements OnRequest {
onRequest(moduleInfo) {
receivedRequest = moduleInfo.request
}
}
const { schema, context } = new GraphQLModule({
typeDefs: gql`
type Query {
foo: String
}
`,
resolvers: {
Query: {
foo: (root, args, { injector }: ModuleContext, info) =>
injector.get(ModuleSessionInfo).request.foo,
},
},
providers: [FooProvider],
})
const result = await execute({
schema,
document: gql`
query {
foo
}
`,
contextValue: await context(fooRequest),
})
expect(result.errors).toBeFalsy()
expect(receivedRequest).toBe(fooRequest)
expect(result.data.foo).toBe(fooRequest.foo)
})
})
describe('Resolvers Composition', async () => {
it('should call resolvers composition with module context', async () => {
const schema = app.schema
const context = await app.context({ req: {} })
const result = await execute({
schema,
document: testQuery,
contextValue: context,
})
expect(resolverCompositionCalled).toBe(true)
})
it('should call resolvers composition in correct order with correct context', async () => {
const { schema, context } = new GraphQLModule({
typeDefs: `
type Query {
foo: String
}
`,
context: async () => {
return {
counter: 0,
foo: null,
bar: null,
}
},
resolvers: {
Query: {
foo: (root, args, context, info) => {
context.counter++
expect(context.foo).toBe('bar')
expect(context.bar).toBe('foo')
expect(context.counter).toBe(3)
return 'Hello'
},
},
},
resolversComposition: {
'Query.foo': [
next => (root, args, context, info) => {
context.counter++
context.foo = 'bar'
expect(context.counter).toBe(1)
return next(root, args, context, info)
},
next => (root, args, context, info) => {
context.counter++
expect(context.foo).toBe('bar')
expect(context.counter).toBe(2)
context.bar = 'foo'
return next(root, args, context, info)
},
],
},
})
const result = await execute({
schema,
document: gql`
query {
foo
}
`,
contextValue: await context({ req: {} }),
})
expect(result.errors).toBeFalsy()
expect(result.data.foo).toBe('Hello')
})
it('should inject context correctly into `__resolveType`', async () => {
let hasInjector = false
const { schema, context } = new GraphQLModule({
typeDefs: `
type Query {
something: MyBase
}
interface MyBase {
id: String
}
type MyType implements MyBase {
id: String
}
`,
resolvers: {
Query: {
something: () => {
return { someValue: 1 }
},
},
MyBase: {
__resolveType: (obj, context) => {
hasInjector = !!context.injector
return 'MyType'
},
},
MyType: {
id: o => o.someValue,
},
},
})
const contextValue = await context({ req: {} })
await execute({
schema,
document: gql`
query {
something {
id
}
}
`,
contextValue,
})
expect(hasInjector).toBeTruthy()
})
})
describe('Schema Directives', async () => {
it('should handle schema directives', async () => {
const typeDefs = gql`
directive @date on FIELD_DEFINITION
scalar Date
type Query {
today: Date @date
}
`
class FormattableDateDirective extends SchemaDirectiveVisitor {
public visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field
field.args.push({
name: 'format',
type: GraphQLString,
})
field.resolve = async function(source, args, context, info) {
const date = await resolve.call(this, source, args, context, info)
return date.toLocaleDateString()
}
field.type = GraphQLString
}
}
const { schema, context } = new GraphQLModule({
typeDefs,
resolvers: {
Query: {
today: () => new Date(),
},
},
schemaDirectives: {
date: FormattableDateDirective,
},
})
const contextValue = await context({ req: {} })
const result = await execute({
schema,
document: gql`
query {
today
}
`,
contextValue,
})
expect(result.data.today).toEqual(new Date().toLocaleDateString())
})
})
describe('Providers Scope', async () => {
it('should construct session scope on each network request', async () => {
let counter = 0
@Injectable({
scope: ProviderScope.Session,
})
class ProviderA {
constructor() {
counter++
}
test(injector: Injector) {
return this === injector.get(ProviderA)
}
}
const { schema, context } = new GraphQLModule({
typeDefs: gql`
type Query {
test: Boolean
}
`,
resolvers: {
Query: {
test: (root: never, args: never, { injector }: ModuleContext) =>
injector.get(ProviderA).test(injector),
},
} as any,
providers: [ProviderA],
})
expect(counter).toBe(0)
const result1 = await execute({
schema,
document: gql`
query {
test
}
`,
contextValue: await context({ req: {} }),
})
expect(result1.data.test).toBe(true)
expect(counter).toBe(1)
const result2 = await execute({
schema,
document: gql`
query {
test
}
`,
contextValue: await context({ req: {} }),
})
expect(result2.data.test).toBe(true)
expect(counter).toBe(2)
})
it('should construct request scope on each injector request independently from network request', async () => {
let counter = 0
@Injectable({
scope: ProviderScope.Request,
})
class ProviderA {
constructor() {
counter++
}
}
const { context, injector } = new GraphQLModule({
providers: [ProviderA],
})
expect(counter).toBe(0)
await context({ mustBe: 0 })
expect(counter).toBe(0)
injector.get(ProviderA)
expect(counter).toBe(1)
injector.get(ProviderA)
expect(counter).toBe(2)
})
it('should inject network request with moduleSessionInfo in session and request scope providers', async () => {
const testRequest = {
foo: 'BAR',
}
@Injectable({
scope: ProviderScope.Session,
})
class ProviderA {
constructor(private moduleInfo: ModuleSessionInfo) {}
test() {
return this.moduleInfo.request.foo
}
}
@Injectable({
scope: ProviderScope.Request,
})
class ProviderB {
constructor(private moduleInfo: ModuleSessionInfo) {}
test() {
return this.moduleInfo.request.foo
}
}
const { schema, context } = new GraphQLModule({
typeDefs: gql`
type Query {
testA: String
testB: String
}
`,
resolvers: {
Query: {
testA: (root: never, args: never, { injector }: ModuleContext) =>
injector.get(ProviderA).test(),
testB: (root: never, args: never, { injector }: ModuleContext) =>
injector.get(ProviderB).test(),
},
} as any,
providers: [ProviderA, ProviderB],
})
const result = await execute({
schema,
document: gql`
query {
testA
testB
}
`,
contextValue: await context(testRequest),
})
expect(result.errors).toBeFalsy()
expect(result.data.testA).toBe('BAR')
expect(result.data.testB).toBe('BAR')
})
})
describe('Extra Schemas', async () => {
it('should handle extraSchemas together with local ones', async () => {
const extraSchema = makeExecutableSchema({
typeDefs: gql`
directive @myDirective on FIELD_DEFINITION
type Query {
foo: Foo
}
type Foo {
id: ID
content: String
}
`,
resolvers: {
Query: {
foo: () => ({
content: 'FOO',
}),
},
},
})
const { schema, context } = new GraphQLModule({
typeDefs: gql`
type Query {
bar: Bar
}
type Bar {
id: ID @myDirective
content: String
}
`,
resolvers: {
Query: {
bar: () => ({}),
},
Bar: {
content: () => 'BAR',
},
},
extraSchemas: [extraSchema],
})
const contextValue = await context({ req: {} })
const result = await execute({
schema,
document: gql`
query {
foo {
content
}
bar {
content
}
}
`,
contextValue,
})
expect(result.errors).toBeFalsy()
expect(result.data.foo.content).toBe('FOO')
expect(result.data.bar.content).toBe('BAR')
})
})
it('should mutate schema using middleware', async () => {
const { schema, context } = new GraphQLModule({
typeDefs: gql`
type Query {
foo: Boolean
}
`,
resolvers: {
Query: {
foo: (root, args, context, info) => !!info.schema.__DIRTY__,
},
},
middleware: ({ schema }) => {
;(schema as any).__DIRTY__ = true
return { schema }
},
})
const result = await execute({
schema,
document: gql`
query {
foo
}
`,
contextValue: await context({ req: {} }),
})
expect(result.errors).toBeFalsy()
expect(result.data.foo).toBeTruthy()
})
it('should avoid getting non-configured module', async () => {
const FOO = Symbol('FOO')
const moduleA = new GraphQLModule<{ foo: string }>({
providers: ({ config }) => [
{
provide: FOO,
useValue: config.foo,
},
],
configRequired: true,
})
const moduleB = new GraphQLModule({
typeDefs: gql`
type Query {
foo: String
}
`,
resolvers: {
Query: {
foo: (_, __, { injector }) => injector.get(FOO),
},
},
imports: [moduleA],
})
const { schema, context } = new GraphQLModule({
imports: [
moduleB,
moduleA.forRoot({
foo: 'FOO',
}),
],
})
const result = await execute({
schema,
document: gql`
query {
foo
}
`,
contextValue: await context({ req: {} }),
})
expect(result.errors).toBeFalsy()
expect(result.data.foo).toBe('FOO')
})
it('should export correct typeDefs and resolvers', async () => {
const gqlModule = new GraphQLModule({
imports: [
new GraphQLModule({
name: 'test',
typeDefs: 'type Query { test: Int }',
resolvers: {
Query: {
test: () => 1,
},
},
}),
],
})
const typeDefs = gqlModule.typeDefs
expect(stripWhiteSpaces(print(typeDefs))).toBe(
stripWhiteSpaces('type Query { test: Int }')
)
const context = await gqlModule.context({})
const resolvers = gqlModule.resolvers
expect(await (resolvers.Query as any).test(null, {}, context, {})).toBe(1)
})
it('should resolve scalars correctly', async () => {
const today = new Date()
const { schema, context } = new GraphQLModule({
typeDefs: gql`
scalar Date
type Query {
today: Date
}
`,
resolvers: {
Date: new GraphQLScalarType({
name: 'Date',
description: 'Date custom scalar type',
parseValue(value) {
return new Date(value) // value from the client
},
serialize(value) {
return value.getTime() // value sent to the client
},
parseLiteral(ast) {
if (ast.kind === Kind.INT) {
return new Date(ast.value) // ast value is always in string format
}
return null
},
}),
Query: {
today: () => today,
},
},
})
const result = await execute({
schema,
document: gql`
query {
today
}
`,
contextValue: await context({ req: {} }),
})
expect(result.errors).toBeFalsy()
expect(result.data.today).toBe(today.getTime())
})
describe('Apollo DataSources Integration', () => {
it('Should pass props correctly to initialize method', async () => {
@Injectable({
scope: ProviderScope.Session,
})
class TestDataSourceAPI {
public initialize(initParams: ModuleSessionInfo) {
expect(initParams.context.myField).toBe('some-value')
expect(initParams.module).toBe(moduleA)
expect(initParams.cache).toBe(moduleA.cache)
}
}
const testQuery = gql`
query {
a {
f
}
}
`
const typesA = [`type A { f: String}`, `type Query { a: A }`]
const moduleA = new GraphQLModule({
name: 'A',
typeDefs: typesA,
resolvers: {
Query: { a: () => ({ f: 's' }) },
},
context: () => {
return {
myField: 'some-value',
}
},
providers: [TestDataSourceAPI],
})
const app = new GraphQLModule({ imports: [moduleA] })
await app.context({ req: {} })
})
})
it('should exclude network request', async () => {
const { schema, context } = new GraphQLModule({
context: {
request: { foo: 'BAR' },
// this request is not request that is internally passed by GraphQLModules
// this request must be passed instead of Network Request
},
typeDefs: gql`
type Query {
foo: String
}
`,
resolvers: {
Query: {
foo: (_, __, context) => {
return context.request.foo
},
},
},
})
const result = await execute({
schema,
document: gql`
query {
foo
}
`,
contextValue: await context({ req: {} }),
})
expect(result.errors).toBeFalsy()
expect(result.data.foo).toBe('BAR')
})
})