Skip to content

Commit

Permalink
Added mockModule and mockApplication to testkit + custom GraphQLSchem…
Browse files Browse the repository at this point in the history
…a 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 <kamil.kisiela@gmail.com>
  • Loading branch information
dotansimha and kamilkisiela committed Jan 7, 2021
1 parent c5bb142 commit f38ff90
Show file tree
Hide file tree
Showing 19 changed files with 699 additions and 99 deletions.
5 changes: 5 additions & 0 deletions .changeset/flat-teachers-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphql-modules': minor
---

Introduce testkit.mockApplication
5 changes: 5 additions & 0 deletions .changeset/selfish-yaks-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphql-modules': minor
---

Custom GraphQLSchema builder
5 changes: 5 additions & 0 deletions .changeset/witty-apricots-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphql-modules': minor
---

Introduce testkit.mockModule
9 changes: 9 additions & 0 deletions packages/graphql-modules/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
{
"name": "graphql-modules",
"description": "Create reusable, maintainable, testable and extendable GraphQL modules",
"keywords": [
"graphql",
"graphql-modules",
"server",
"typescript",
"the-guild"
],
"version": "1.1.0",
"author": "Kamil Kisiela",
"license": "MIT",
Expand All @@ -19,6 +27,7 @@
"dependencies": {
"@graphql-tools/schema": "^7.0.0",
"@graphql-tools/wrap": "^7.0.0",
"@graphql-typed-document-node/core": "^3.1.0",
"ramda": "^0.27.1"
},
"publishConfig": {
Expand Down
204 changes: 118 additions & 86 deletions packages/graphql-modules/src/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
} from '../di';
import { ResolvedModule } from '../module/factory';
import { ID } from '../shared/types';
import { ModuleDuplicatedError } from '../shared/errors';
import {
ModuleDuplicatedError,
ModuleNonUniqueIdError,
} from '../shared/errors';
import { flatten, isDefined } from '../shared/utils';
import { ApplicationConfig, Application } from './types';
import {
Expand All @@ -19,6 +22,7 @@ import { createContextBuilder } from './context';
import { executionCreator } from './execution';
import { subscriptionCreator } from './subscription';
import { apolloSchemaCreator, apolloExecutorCreator } from './apollo';
import { Module } from '../module/types';

export type ModulesMap = Map<ID, ResolvedModule>;

Expand Down Expand Up @@ -53,92 +57,107 @@ export interface InternalAppContext {
* })
* ```
*/
export function createApplication(config: ApplicationConfig): Application {
const providers =
config.providers && typeof config.providers === 'function'
? config.providers()
: config.providers;
// Creates an Injector with singleton classes at application level
const appSingletonProviders = ReflectiveInjector.resolve(
onlySingletonProviders(providers)
);
const appInjector = ReflectiveInjector.createFromResolved({
name: 'App (Singleton Scope)',
providers: appSingletonProviders,
});
// Filter Operation-scoped providers, and keep it here
// so we don't do it over and over again
const appOperationProviders = ReflectiveInjector.resolve(
onlyOperationProviders(providers)
);
const middlewareMap = config.middlewares || {};

// Create all modules
const modules = config.modules.map((mod) =>
mod.factory({
export function createApplication(
applicationConfig: ApplicationConfig
): Application {
function applicationFactory(cfg?: ApplicationConfig): Application {
const config = cfg || applicationConfig;
const providers =
config.providers && typeof config.providers === 'function'
? config.providers()
: config.providers;
// Creates an Injector with singleton classes at application level
const appSingletonProviders = ReflectiveInjector.resolve(
onlySingletonProviders(providers)
);
const appInjector = ReflectiveInjector.createFromResolved({
name: 'App (Singleton Scope)',
providers: appSingletonProviders,
});
// Filter Operation-scoped providers, and keep it here
// so we don't do it over and over again
const appOperationProviders = ReflectiveInjector.resolve(
onlyOperationProviders(providers)
);
const middlewareMap = config.middlewares || {};

// Validations
ensureModuleUniqueIds(config.modules);

// Create all modules
const modules = config.modules.map((mod) =>
mod.factory({
injector: appInjector,
middlewares: middlewareMap,
})
);
const modulesMap = createModulesMap(modules);
const singletonGlobalProvidersMap = createGlobalProvidersMap({
modules,
scope: Scope.Singleton,
});
const operationGlobalProvidersMap = createGlobalProvidersMap({
modules,
scope: Scope.Operation,
});

attachGlobalProvidersMap({
injector: appInjector,
globalProvidersMap: singletonGlobalProvidersMap,
moduleInjectorGetter(moduleId) {
return modulesMap.get(moduleId)!.injector;
},
});

// Creating a schema, flattening the typedefs and resolvers
// is not expensive since it happens only once
const typeDefs = flatten(modules.map((mod) => mod.typeDefs));
const resolvers = modules.map((mod) => mod.resolvers).filter(isDefined);
const schema = (applicationConfig.schemaBuilder || makeExecutableSchema)({
typeDefs,
resolvers,
});

const contextBuilder = createContextBuilder({
appInjector,
appLevelOperationProviders: appOperationProviders,
modulesMap: modulesMap,
singletonGlobalProvidersMap,
operationGlobalProvidersMap,
});

const createSubscription = subscriptionCreator({ contextBuilder });
const createExecution = executionCreator({ contextBuilder });
const createSchemaForApollo = apolloSchemaCreator({
createSubscription,
contextBuilder,
schema,
});
const createApolloExecutor = apolloExecutorCreator({
createExecution,
schema,
});

instantiateSingletonProviders({
appInjector,
modulesMap,
});

return {
typeDefs,
resolvers,
schema,
injector: appInjector,
middlewares: middlewareMap,
})
);
const modulesMap = createModulesMap(modules);
const singletonGlobalProvidersMap = createGlobalProvidersMap({
modules,
scope: Scope.Singleton,
});
const operationGlobalProvidersMap = createGlobalProvidersMap({
modules,
scope: Scope.Operation,
});

attachGlobalProvidersMap({
injector: appInjector,
globalProvidersMap: singletonGlobalProvidersMap,
moduleInjectorGetter(moduleId) {
return modulesMap.get(moduleId)!.injector;
},
});

// Creating a schema, flattening the typedefs and resolvers
// is not expensive since it happens only once
const typeDefs = flatten(modules.map((mod) => mod.typeDefs));
const resolvers = modules.map((mod) => mod.resolvers).filter(isDefined);
const schema = makeExecutableSchema({ typeDefs, resolvers });

const contextBuilder = createContextBuilder({
appInjector,
appLevelOperationProviders: appOperationProviders,
modulesMap: modulesMap,
singletonGlobalProvidersMap,
operationGlobalProvidersMap,
});

const createSubscription = subscriptionCreator({ contextBuilder });
const createExecution = executionCreator({ contextBuilder });
const createSchemaForApollo = apolloSchemaCreator({
createSubscription,
contextBuilder,
schema,
});
const createApolloExecutor = apolloExecutorCreator({
createExecution,
schema,
});

instantiateSingletonProviders({
appInjector,
modulesMap,
});

return {
typeDefs,
resolvers,
schema,
injector: appInjector,
createSubscription,
createExecution,
createSchemaForApollo,
createApolloExecutor,
};
createSubscription,
createExecution,
createSchemaForApollo,
createApolloExecutor,
ɵfactory: applicationFactory,
ɵconfig: config,
};
}

return applicationFactory();
}

function createModulesMap(modules: ResolvedModule[]): ModulesMap {
Expand Down Expand Up @@ -170,3 +189,16 @@ function createModulesMap(modules: ResolvedModule[]): ModulesMap {

return modulesMap;
}

function ensureModuleUniqueIds(modules: Module[]) {
const collisions = modules
.filter((mod, i, all) => i !== all.findIndex((m) => m.id === mod.id))
.map((m) => m.id);

if (collisions.length) {
throw new ModuleNonUniqueIdError(
`Modules with non-unique ids: ${collisions.join(', ')}`,
`All modules should have unique ids, please locate and fix them.`
);
}
}
38 changes: 35 additions & 3 deletions packages/graphql-modules/src/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ExecutionResult,
} from 'graphql';
import { Provider, Injector } from '../di';
import { Resolvers, Module } from '../module/types';
import { Resolvers, Module, MockedModule } from '../module/types';
import { Single, ValueOrPromise } from '../shared/types';
import { MiddlewareMap } from '../shared/middleware';
import { ApolloRequestContext } from './apollo';
Expand All @@ -17,11 +17,16 @@ export type ApolloExecutor = (
requestContext: ApolloRequestContext
) => ValueOrPromise<ExecutionResult>;

export interface MockedApplication extends Application {
replaceModule(mockedModule: MockedModule): MockedApplication;
addProviders(providers: ApplicationConfig['providers']): MockedApplication;
}

/**
* @api
* A return type of `createApplication` function.
*/
export type Application = {
export interface Application {
/**
* A list of type definitions defined by modules.
*/
Expand Down Expand Up @@ -56,7 +61,15 @@ export type Application = {
* Experimental
*/
createApolloExecutor(): ApolloExecutor;
};
/**
* @internal
*/
ɵfactory(config?: ApplicationConfig | undefined): Application;
/**
* @internal
*/
ɵconfig: ApplicationConfig;
}

/**
* @api
Expand All @@ -75,4 +88,23 @@ export interface ApplicationConfig {
* A map of middlewares - read the ["Middlewares"](./advanced/middlewares) chapter.
*/
middlewares?: MiddlewareMap;
/**
* Creates a GraphQLSchema object out of typeDefs and resolvers
*
* @example
*
* ```typescript
* import { createApplication } from 'graphql-modules';
* import { makeExecutableSchema } from '@graphql-tools/schema';
*
* const app = createApplication({
* modules: [],
* schemaBuilder: makeExecutableSchema
* })
* ```
*/
schemaBuilder?(input: {
typeDefs: DocumentNode[];
resolvers: Record<string, any>[];
}): GraphQLSchema;
}
7 changes: 7 additions & 0 deletions packages/graphql-modules/src/module/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,10 @@ export interface Module {
singletonProviders: ResolvedProvider[];
config: ModuleConfig;
}

export interface MockedModule extends Module {
/**
* @internal
*/
ɵoriginalModule: Module;
}
8 changes: 8 additions & 0 deletions packages/graphql-modules/src/shared/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { ID } from './types';

export class ModuleNonUniqueIdError extends ExtendableBuiltin(Error) {
constructor(message: string, ...rest: string[]) {
super(composeMessage(message, ...rest));
this.name = this.constructor.name;
this.message = composeMessage(message, ...rest);
}
}

export class ModuleDuplicatedError extends ExtendableBuiltin(Error) {
constructor(message: string, ...rest: string[]) {
super(composeMessage(message, ...rest));
Expand Down
12 changes: 12 additions & 0 deletions packages/graphql-modules/src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ export function once(cb: () => void) {
};
}

export function share<T, A>(factory: (arg?: A) => T): (arg?: A) => T {
let cached: T | null = null;

return (arg?: A) => {
if (!cached) {
cached = factory(arg);
}

return cached;
};
}

export function uniqueId(isNotUsed: (id: string) => boolean) {
let id: string;

Expand Down

0 comments on commit f38ff90

Please sign in to comment.