Skip to content

Commit f38ff90

Browse files
Added mockModule and mockApplication to testkit + custom GraphQLSchema builder (#1512)
* Added testkit.mockModule * Added testkit.mockApplication * Custom schema builder * fail when modules have non-unique ids * Support TypedDocumentNode Co-authored-by: Kamil Kisiela <[email protected]>
1 parent c5bb142 commit f38ff90

File tree

19 files changed

+699
-99
lines changed

19 files changed

+699
-99
lines changed

.changeset/flat-teachers-grow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'graphql-modules': minor
3+
---
4+
5+
Introduce testkit.mockApplication

.changeset/selfish-yaks-speak.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'graphql-modules': minor
3+
---
4+
5+
Custom GraphQLSchema builder

.changeset/witty-apricots-joke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'graphql-modules': minor
3+
---
4+
5+
Introduce testkit.mockModule

packages/graphql-modules/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
{
22
"name": "graphql-modules",
3+
"description": "Create reusable, maintainable, testable and extendable GraphQL modules",
4+
"keywords": [
5+
"graphql",
6+
"graphql-modules",
7+
"server",
8+
"typescript",
9+
"the-guild"
10+
],
311
"version": "1.1.0",
412
"author": "Kamil Kisiela",
513
"license": "MIT",
@@ -19,6 +27,7 @@
1927
"dependencies": {
2028
"@graphql-tools/schema": "^7.0.0",
2129
"@graphql-tools/wrap": "^7.0.0",
30+
"@graphql-typed-document-node/core": "^3.1.0",
2231
"ramda": "^0.27.1"
2332
},
2433
"publishConfig": {

packages/graphql-modules/src/application/application.ts

Lines changed: 118 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
} from '../di';
88
import { ResolvedModule } from '../module/factory';
99
import { ID } from '../shared/types';
10-
import { ModuleDuplicatedError } from '../shared/errors';
10+
import {
11+
ModuleDuplicatedError,
12+
ModuleNonUniqueIdError,
13+
} from '../shared/errors';
1114
import { flatten, isDefined } from '../shared/utils';
1215
import { ApplicationConfig, Application } from './types';
1316
import {
@@ -19,6 +22,7 @@ import { createContextBuilder } from './context';
1922
import { executionCreator } from './execution';
2023
import { subscriptionCreator } from './subscription';
2124
import { apolloSchemaCreator, apolloExecutorCreator } from './apollo';
25+
import { Module } from '../module/types';
2226

2327
export type ModulesMap = Map<ID, ResolvedModule>;
2428

@@ -53,92 +57,107 @@ export interface InternalAppContext {
5357
* })
5458
* ```
5559
*/
56-
export function createApplication(config: ApplicationConfig): Application {
57-
const providers =
58-
config.providers && typeof config.providers === 'function'
59-
? config.providers()
60-
: config.providers;
61-
// Creates an Injector with singleton classes at application level
62-
const appSingletonProviders = ReflectiveInjector.resolve(
63-
onlySingletonProviders(providers)
64-
);
65-
const appInjector = ReflectiveInjector.createFromResolved({
66-
name: 'App (Singleton Scope)',
67-
providers: appSingletonProviders,
68-
});
69-
// Filter Operation-scoped providers, and keep it here
70-
// so we don't do it over and over again
71-
const appOperationProviders = ReflectiveInjector.resolve(
72-
onlyOperationProviders(providers)
73-
);
74-
const middlewareMap = config.middlewares || {};
75-
76-
// Create all modules
77-
const modules = config.modules.map((mod) =>
78-
mod.factory({
60+
export function createApplication(
61+
applicationConfig: ApplicationConfig
62+
): Application {
63+
function applicationFactory(cfg?: ApplicationConfig): Application {
64+
const config = cfg || applicationConfig;
65+
const providers =
66+
config.providers && typeof config.providers === 'function'
67+
? config.providers()
68+
: config.providers;
69+
// Creates an Injector with singleton classes at application level
70+
const appSingletonProviders = ReflectiveInjector.resolve(
71+
onlySingletonProviders(providers)
72+
);
73+
const appInjector = ReflectiveInjector.createFromResolved({
74+
name: 'App (Singleton Scope)',
75+
providers: appSingletonProviders,
76+
});
77+
// Filter Operation-scoped providers, and keep it here
78+
// so we don't do it over and over again
79+
const appOperationProviders = ReflectiveInjector.resolve(
80+
onlyOperationProviders(providers)
81+
);
82+
const middlewareMap = config.middlewares || {};
83+
84+
// Validations
85+
ensureModuleUniqueIds(config.modules);
86+
87+
// Create all modules
88+
const modules = config.modules.map((mod) =>
89+
mod.factory({
90+
injector: appInjector,
91+
middlewares: middlewareMap,
92+
})
93+
);
94+
const modulesMap = createModulesMap(modules);
95+
const singletonGlobalProvidersMap = createGlobalProvidersMap({
96+
modules,
97+
scope: Scope.Singleton,
98+
});
99+
const operationGlobalProvidersMap = createGlobalProvidersMap({
100+
modules,
101+
scope: Scope.Operation,
102+
});
103+
104+
attachGlobalProvidersMap({
105+
injector: appInjector,
106+
globalProvidersMap: singletonGlobalProvidersMap,
107+
moduleInjectorGetter(moduleId) {
108+
return modulesMap.get(moduleId)!.injector;
109+
},
110+
});
111+
112+
// Creating a schema, flattening the typedefs and resolvers
113+
// is not expensive since it happens only once
114+
const typeDefs = flatten(modules.map((mod) => mod.typeDefs));
115+
const resolvers = modules.map((mod) => mod.resolvers).filter(isDefined);
116+
const schema = (applicationConfig.schemaBuilder || makeExecutableSchema)({
117+
typeDefs,
118+
resolvers,
119+
});
120+
121+
const contextBuilder = createContextBuilder({
122+
appInjector,
123+
appLevelOperationProviders: appOperationProviders,
124+
modulesMap: modulesMap,
125+
singletonGlobalProvidersMap,
126+
operationGlobalProvidersMap,
127+
});
128+
129+
const createSubscription = subscriptionCreator({ contextBuilder });
130+
const createExecution = executionCreator({ contextBuilder });
131+
const createSchemaForApollo = apolloSchemaCreator({
132+
createSubscription,
133+
contextBuilder,
134+
schema,
135+
});
136+
const createApolloExecutor = apolloExecutorCreator({
137+
createExecution,
138+
schema,
139+
});
140+
141+
instantiateSingletonProviders({
142+
appInjector,
143+
modulesMap,
144+
});
145+
146+
return {
147+
typeDefs,
148+
resolvers,
149+
schema,
79150
injector: appInjector,
80-
middlewares: middlewareMap,
81-
})
82-
);
83-
const modulesMap = createModulesMap(modules);
84-
const singletonGlobalProvidersMap = createGlobalProvidersMap({
85-
modules,
86-
scope: Scope.Singleton,
87-
});
88-
const operationGlobalProvidersMap = createGlobalProvidersMap({
89-
modules,
90-
scope: Scope.Operation,
91-
});
92-
93-
attachGlobalProvidersMap({
94-
injector: appInjector,
95-
globalProvidersMap: singletonGlobalProvidersMap,
96-
moduleInjectorGetter(moduleId) {
97-
return modulesMap.get(moduleId)!.injector;
98-
},
99-
});
100-
101-
// Creating a schema, flattening the typedefs and resolvers
102-
// is not expensive since it happens only once
103-
const typeDefs = flatten(modules.map((mod) => mod.typeDefs));
104-
const resolvers = modules.map((mod) => mod.resolvers).filter(isDefined);
105-
const schema = makeExecutableSchema({ typeDefs, resolvers });
106-
107-
const contextBuilder = createContextBuilder({
108-
appInjector,
109-
appLevelOperationProviders: appOperationProviders,
110-
modulesMap: modulesMap,
111-
singletonGlobalProvidersMap,
112-
operationGlobalProvidersMap,
113-
});
114-
115-
const createSubscription = subscriptionCreator({ contextBuilder });
116-
const createExecution = executionCreator({ contextBuilder });
117-
const createSchemaForApollo = apolloSchemaCreator({
118-
createSubscription,
119-
contextBuilder,
120-
schema,
121-
});
122-
const createApolloExecutor = apolloExecutorCreator({
123-
createExecution,
124-
schema,
125-
});
126-
127-
instantiateSingletonProviders({
128-
appInjector,
129-
modulesMap,
130-
});
131-
132-
return {
133-
typeDefs,
134-
resolvers,
135-
schema,
136-
injector: appInjector,
137-
createSubscription,
138-
createExecution,
139-
createSchemaForApollo,
140-
createApolloExecutor,
141-
};
151+
createSubscription,
152+
createExecution,
153+
createSchemaForApollo,
154+
createApolloExecutor,
155+
ɵfactory: applicationFactory,
156+
ɵconfig: config,
157+
};
158+
}
159+
160+
return applicationFactory();
142161
}
143162

144163
function createModulesMap(modules: ResolvedModule[]): ModulesMap {
@@ -170,3 +189,16 @@ function createModulesMap(modules: ResolvedModule[]): ModulesMap {
170189

171190
return modulesMap;
172191
}
192+
193+
function ensureModuleUniqueIds(modules: Module[]) {
194+
const collisions = modules
195+
.filter((mod, i, all) => i !== all.findIndex((m) => m.id === mod.id))
196+
.map((m) => m.id);
197+
198+
if (collisions.length) {
199+
throw new ModuleNonUniqueIdError(
200+
`Modules with non-unique ids: ${collisions.join(', ')}`,
201+
`All modules should have unique ids, please locate and fix them.`
202+
);
203+
}
204+
}

packages/graphql-modules/src/application/types.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
ExecutionResult,
77
} from 'graphql';
88
import { Provider, Injector } from '../di';
9-
import { Resolvers, Module } from '../module/types';
9+
import { Resolvers, Module, MockedModule } from '../module/types';
1010
import { Single, ValueOrPromise } from '../shared/types';
1111
import { MiddlewareMap } from '../shared/middleware';
1212
import { ApolloRequestContext } from './apollo';
@@ -17,11 +17,16 @@ export type ApolloExecutor = (
1717
requestContext: ApolloRequestContext
1818
) => ValueOrPromise<ExecutionResult>;
1919

20+
export interface MockedApplication extends Application {
21+
replaceModule(mockedModule: MockedModule): MockedApplication;
22+
addProviders(providers: ApplicationConfig['providers']): MockedApplication;
23+
}
24+
2025
/**
2126
* @api
2227
* A return type of `createApplication` function.
2328
*/
24-
export type Application = {
29+
export interface Application {
2530
/**
2631
* A list of type definitions defined by modules.
2732
*/
@@ -56,7 +61,15 @@ export type Application = {
5661
* Experimental
5762
*/
5863
createApolloExecutor(): ApolloExecutor;
59-
};
64+
/**
65+
* @internal
66+
*/
67+
ɵfactory(config?: ApplicationConfig | undefined): Application;
68+
/**
69+
* @internal
70+
*/
71+
ɵconfig: ApplicationConfig;
72+
}
6073

6174
/**
6275
* @api
@@ -75,4 +88,23 @@ export interface ApplicationConfig {
7588
* A map of middlewares - read the ["Middlewares"](./advanced/middlewares) chapter.
7689
*/
7790
middlewares?: MiddlewareMap;
91+
/**
92+
* Creates a GraphQLSchema object out of typeDefs and resolvers
93+
*
94+
* @example
95+
*
96+
* ```typescript
97+
* import { createApplication } from 'graphql-modules';
98+
* import { makeExecutableSchema } from '@graphql-tools/schema';
99+
*
100+
* const app = createApplication({
101+
* modules: [],
102+
* schemaBuilder: makeExecutableSchema
103+
* })
104+
* ```
105+
*/
106+
schemaBuilder?(input: {
107+
typeDefs: DocumentNode[];
108+
resolvers: Record<string, any>[];
109+
}): GraphQLSchema;
78110
}

packages/graphql-modules/src/module/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,10 @@ export interface Module {
5050
singletonProviders: ResolvedProvider[];
5151
config: ModuleConfig;
5252
}
53+
54+
export interface MockedModule extends Module {
55+
/**
56+
* @internal
57+
*/
58+
ɵoriginalModule: Module;
59+
}

packages/graphql-modules/src/shared/errors.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { ID } from './types';
22

3+
export class ModuleNonUniqueIdError extends ExtendableBuiltin(Error) {
4+
constructor(message: string, ...rest: string[]) {
5+
super(composeMessage(message, ...rest));
6+
this.name = this.constructor.name;
7+
this.message = composeMessage(message, ...rest);
8+
}
9+
}
10+
311
export class ModuleDuplicatedError extends ExtendableBuiltin(Error) {
412
constructor(message: string, ...rest: string[]) {
513
super(composeMessage(message, ...rest));

packages/graphql-modules/src/shared/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ export function once(cb: () => void) {
8484
};
8585
}
8686

87+
export function share<T, A>(factory: (arg?: A) => T): (arg?: A) => T {
88+
let cached: T | null = null;
89+
90+
return (arg?: A) => {
91+
if (!cached) {
92+
cached = factory(arg);
93+
}
94+
95+
return cached;
96+
};
97+
}
98+
8799
export function uniqueId(isNotUsed: (id: string) => boolean) {
88100
let id: string;
89101

0 commit comments

Comments
 (0)