Skip to content

Commit

Permalink
Manual control of GraphQL operation (#1568)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela authored Mar 2, 2021
1 parent e00067b commit 499c81a
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 38 deletions.
5 changes: 5 additions & 0 deletions .changeset/twelve-flies-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphql-modules': minor
---

Manual control of GraphQL operation
20 changes: 10 additions & 10 deletions packages/graphql-modules/src/application/apollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DocumentNode, execute, GraphQLSchema } from 'graphql';
import { uniqueId } from '../shared/utils';
import { InternalAppContext } from './application';
import { ExecutionContextBuilder } from './context';
import { ApolloExecutor, Application } from './types';
import { Application } from './types';

const CONTEXT_ID = Symbol.for('context-id');

Expand All @@ -22,9 +22,9 @@ export function apolloExecutorCreator({
}: {
createExecution: Application['createExecution'];
schema: GraphQLSchema;
}): () => ApolloExecutor {
return function createApolloExecutor() {
const executor = createExecution();
}): Application['createApolloExecutor'] {
return function createApolloExecutor(options) {
const executor = createExecution(options);
return function executorAdapter(requestContext: ApolloRequestContext) {
return executor({
schema,
Expand Down Expand Up @@ -52,7 +52,7 @@ export function apolloSchemaCreator({
{
count: number;
session: {
onDestroy(): void;
destroy(): void;
context: InternalAppContext;
};
}
Expand All @@ -62,15 +62,15 @@ export function apolloSchemaCreator({
function getSession(ctx: any) {
if (!ctx[CONTEXT_ID]) {
ctx[CONTEXT_ID] = uniqueId((id) => !sessions[id]);
const { context, onDestroy } = contextBuilder(ctx);
const { context, ɵdestroy: destroy } = contextBuilder(ctx);

sessions[ctx[CONTEXT_ID]] = {
count: 0,
session: {
context,
onDestroy() {
destroy() {
if (--sessions[ctx[CONTEXT_ID]].count === 0) {
onDestroy();
destroy();
delete sessions[ctx[CONTEXT_ID]];
}
},
Expand All @@ -87,7 +87,7 @@ export function apolloSchemaCreator({
schema,
executor(input) {
// Create an execution context
const { context, onDestroy } = getSession(input.context!);
const { context, destroy } = getSession(input.context!);

// It's important to wrap the executeFn within a promise
// so we can easily control the end of execution (with finally)
Expand All @@ -102,7 +102,7 @@ export function apolloSchemaCreator({
rootValue: input.info?.rootValue,
}) as any
)
.finally(onDestroy);
.finally(destroy);
},
subscriber(input) {
return subscription({
Expand Down
5 changes: 5 additions & 0 deletions packages/graphql-modules/src/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { createContextBuilder } from './context';
import { executionCreator } from './execution';
import { subscriptionCreator } from './subscription';
import { apolloSchemaCreator, apolloExecutorCreator } from './apollo';
import { operationControllerCreator } from './operation-controller';
import { Module } from '../module/types';

export type ModulesMap = Map<ID, ResolvedModule>;
Expand Down Expand Up @@ -126,6 +127,9 @@ export function createApplication(
operationGlobalProvidersMap,
});

const createOperationController = operationControllerCreator({
contextBuilder,
});
const createSubscription = subscriptionCreator({ contextBuilder });
const createExecution = executionCreator({ contextBuilder });
const createSchemaForApollo = apolloSchemaCreator({
Expand All @@ -148,6 +152,7 @@ export function createApplication(
resolvers,
schema,
injector: appInjector,
createOperationController,
createSubscription,
createExecution,
createSchemaForApollo,
Expand Down
11 changes: 6 additions & 5 deletions packages/graphql-modules/src/application/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReflectiveInjector } from '../di';
import { Injector, ReflectiveInjector } from '../di';
import { ResolvedProvider } from '../di/resolution';
import { ID } from '../shared/types';
import { once } from '../shared/utils';
Expand All @@ -15,7 +15,8 @@ export type ExecutionContextBuilder<
context: TContext
) => {
context: InternalAppContext;
onDestroy: () => void;
ɵdestroy(): void;
ɵinjector: Injector;
};

export function createContextBuilder({
Expand Down Expand Up @@ -58,7 +59,6 @@ export function createContextBuilder({
});
}

let operationAppInjector: ReflectiveInjector;
let appContext: GraphQLModules.AppContext;

attachGlobalProvidersMap({
Expand Down Expand Up @@ -98,7 +98,7 @@ export function createContextBuilder({
// As the name of the Injector says, it's an Operation scoped Injector
// Application level
// Operation scoped - means it's created and destroyed on every GraphQL Operation
operationAppInjector = ReflectiveInjector.createFromResolved({
const operationAppInjector = ReflectiveInjector.createFromResolved({
name: 'App (Operation Scope)',
providers: appLevelOperationProviders.concat(
ReflectiveInjector.resolve([
Expand Down Expand Up @@ -178,7 +178,7 @@ export function createContextBuilder({
});

return {
onDestroy: once(() => {
ɵdestroy: once(() => {
providersToDestroy.forEach(([injector, keyId]) => {
// If provider was instantiated
if (injector._isObjectDefinedByKeyId(keyId)) {
Expand All @@ -188,6 +188,7 @@ export function createContextBuilder({
});
contextCache = {};
}),
ɵinjector: operationAppInjector,
context: sharedContext,
};
};
Expand Down
14 changes: 8 additions & 6 deletions packages/graphql-modules/src/application/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ export function executionCreator({
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>
) => {
// Create an execution context
const { context, onDestroy } = contextBuilder(
isNotSchema<ExecutionArgs>(argsOrSchema)
? argsOrSchema.contextValue
: contextValue
);
const { context, ɵdestroy: destroy } =
options?.controller ??
contextBuilder(
isNotSchema<ExecutionArgs>(argsOrSchema)
? argsOrSchema.contextValue
: contextValue
);

const executionArgs: ExecutionArgs = isNotSchema<ExecutionArgs>(
argsOrSchema
Expand All @@ -59,7 +61,7 @@ export function executionCreator({
// so we can easily control the end of execution (with finally)
return Promise.resolve()
.then(() => executeFn(executionArgs))
.finally(onDestroy);
.finally(destroy);
};
};

Expand Down
20 changes: 20 additions & 0 deletions packages/graphql-modules/src/application/operation-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Application } from './types';
import type { ExecutionContextBuilder } from './context';

export function operationControllerCreator(options: {
contextBuilder: ExecutionContextBuilder<GraphQLModules.GlobalContext>;
}): Application['createOperationController'] {
const { contextBuilder } = options;

return (input) => {
const operation = contextBuilder(input.context);
const ɵdestroy = input.autoDestroy ? operation.ɵdestroy : () => {};

return {
context: operation.context,
injector: operation.ɵinjector,
destroy: operation.ɵdestroy,
ɵdestroy,
};
};
}
16 changes: 9 additions & 7 deletions packages/graphql-modules/src/application/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ export function subscriptionCreator({
subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>
) => {
// Create an subscription context
const { context, onDestroy } = contextBuilder(
isNotSchema<SubscriptionArgs>(argsOrSchema)
? argsOrSchema.contextValue
: contextValue
);
const { context, ɵdestroy: destroy } =
options?.controller ??
contextBuilder(
isNotSchema<SubscriptionArgs>(argsOrSchema)
? argsOrSchema.contextValue
: contextValue
);

const subscriptionArgs: SubscriptionArgs = isNotSchema<SubscriptionArgs>(
argsOrSchema
Expand Down Expand Up @@ -67,13 +69,13 @@ export function subscriptionCreator({
.then((sub) => {
if (isAsyncIterable(sub)) {
isIterable = true;
return tapAsyncIterator(sub, onDestroy);
return tapAsyncIterator(sub, destroy);
}
return sub;
})
.finally(() => {
if (!isIterable) {
onDestroy();
destroy();
}
});
};
Expand Down
48 changes: 40 additions & 8 deletions packages/graphql-modules/src/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import {
GraphQLSchema,
ExecutionResult,
} from 'graphql';
import { Provider, Injector } from '../di';
import { Resolvers, Module, MockedModule } from '../module/types';
import { Single, ValueOrPromise } from '../shared/types';
import { MiddlewareMap } from '../shared/middleware';
import { ApolloRequestContext } from './apollo';
import type { Provider, Injector } from '../di';
import type { Resolvers, Module, MockedModule } from '../module/types';
import type { MiddlewareMap } from '../shared/middleware';
import type { ApolloRequestContext } from './apollo';
import type { Single, ValueOrPromise } from '../shared/types';
import type { InternalAppContext } from './application';

type Execution = typeof execute;
type Subscription = typeof subscribe;
Expand Down Expand Up @@ -43,24 +44,39 @@ export interface Application {
* The application (Singleton) injector.
*/
readonly injector: Injector;
/**
* Take over control of GraphQL Operation
*/
createOperationController(input: {
context: any;
autoDestroy?: boolean;
}): OperationController;
/**
* Creates a `subscribe` function that runs the subscription phase of GraphQL.
* Important when using GraphQL Subscriptions.
*/
createSubscription(options?: { subscribe?: typeof subscribe }): Subscription;
createSubscription(options?: {
subscribe?: typeof subscribe;
controller?: OperationController;
}): Subscription;
/**
* Creates a `execute` function that runs the execution phase of GraphQL.
* Important when using GraphQL Queries and Mutations.
*/
createExecution(options?: { execute?: typeof execute }): Execution;
createExecution(options?: {
execute?: typeof execute;
controller?: OperationController;
}): Execution;
/**
* Experimental
*/
createSchemaForApollo(): GraphQLSchema;
/**
* Experimental
*/
createApolloExecutor(): ApolloExecutor;
createApolloExecutor(options?: {
controller?: OperationController;
}): ApolloExecutor;
/**
* @internal
*/
Expand All @@ -71,6 +87,22 @@ export interface Application {
ɵconfig: ApplicationConfig;
}

export interface OperationController {
/**
* Destroys
*/
destroy(): void;
/**
* @internal
*/
ɵdestroy(): void;
context: InternalAppContext;
/**
* Operation Injector (application)
*/
injector: Injector;
}

/**
* @api
* Application's configuration object. Represents the first argument of `createApplication` function.
Expand Down
7 changes: 5 additions & 2 deletions packages/graphql-modules/src/testing/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { DocumentNode, ExecutionArgs, ExecutionResult } from 'graphql';
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import type { Application } from '../application/types';
import type { Application, OperationController } from '../application/types';
import type { ValueOrPromise } from '../shared/types';

export function execute<
Expand All @@ -11,9 +11,12 @@ export function execute<
inputs: Omit<ExecutionArgs, 'schema'> & {
document: DocumentNode | TypedDocumentNode<TResult, TVariables>;
variableValues?: TVariables;
},
options?: {
controller?: OperationController;
}
): ValueOrPromise<ExecutionResult<TResult>> {
const executor = app.createExecution();
const executor = app.createExecution(options);

return executor({
schema: app.schema,
Expand Down
3 changes: 3 additions & 0 deletions packages/graphql-modules/src/testing/test-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export function mockApplication(app: Application): MockedApplication {
get injector() {
return sharedFactory().injector;
},
createOperationController(options) {
return sharedFactory().createOperationController(options);
},
createSubscription(options) {
return sharedFactory().createSubscription(options);
},
Expand Down
Loading

0 comments on commit 499c81a

Please sign in to comment.