Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not merge yet: Make ExecutionContext opt-in #2255

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tame-oranges-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphql-modules': major
---

Make ExecutionContext opt-in and graphql-modules platform agnostic
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest] # remove windows to speed up the tests
node-version: [12, 16, 18]
node-version: [16, 18]
graphql_version:
- 15
- 16
Expand Down
2 changes: 2 additions & 0 deletions benchmark/basic.case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const app = createApplication({
},
}),
],
executionContext: false,
});

class Posts {
Expand Down Expand Up @@ -76,6 +77,7 @@ const appWithDI = createApplication({
},
}),
],
executionContext: false,
});

const pureSchema = makeExecutableSchema({
Expand Down
1 change: 1 addition & 0 deletions examples/apollo-subscriptions/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import { PostModule } from './post/post.module';
export const graphqlApplication = createApplication({
modules: [PostModule],
providers: [PubSub],
executionContext: false,
});
1 change: 1 addition & 0 deletions examples/basic-with-dependency-injection/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { graphqlHTTP } from 'express-graphql';

const app = createApplication({
modules: [BlogModule, UserModule],
executionContext: false,
});

const server = express();
Expand Down
1 change: 1 addition & 0 deletions examples/basic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { SocialNetworkModule } from './app/social-network/social-network.module'
const server = express();
const app = createApplication({
modules: [UserModule, AuthModule, SocialNetworkModule],
executionContext: false,
});
const execute = app.createExecution();

Expand Down
1 change: 1 addition & 0 deletions examples/graphql-yoga/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { PostModule } from './post/post.module';

export const app = createApplication({
modules: [PostModule],
executionContext: false,
});
1 change: 1 addition & 0 deletions examples/subscriptions/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import { PostModule } from './post/post.module';
export const app = createApplication({
providers: [PubSub],
modules: [PostModule],
executionContext: false,
});
10 changes: 9 additions & 1 deletion packages/graphql-modules/src/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
instantiateSingletonProviders,
} from './di';
import { createContextBuilder } from './context';
import { enableExecutionContext } from './execution-context';
import { executionCreator } from './execution';
import { subscriptionCreator } from './subscription';
import { apolloSchemaCreator, apolloExecutorCreator } from './apollo';
Expand All @@ -45,6 +46,7 @@ export interface InternalAppContext {
*
* ```typescript
* import { createApplication } from 'graphql-modules';
* import { createHook, executionAsyncId } from 'async_hooks';
* import { usersModule } from './users';
* import { postsModule } from './posts';
* import { commentsModule } from './comments';
Expand All @@ -54,7 +56,8 @@ export interface InternalAppContext {
* usersModule,
* postsModule,
* commentsModule
* ]
* ],
* executionContext: { createHook, executionAsyncId },
* })
* ```
*/
Expand All @@ -63,6 +66,11 @@ export function createApplication(
): Application {
function applicationFactory(cfg?: ApplicationConfig): Application {
const config = cfg || applicationConfig;

if (config.executionContext) {
enableExecutionContext(config.executionContext);
}

const providers =
config.providers && typeof config.providers === 'function'
? config.providers()
Expand Down
74 changes: 50 additions & 24 deletions packages/graphql-modules/src/application/execution-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { createHook, executionAsyncId } from 'async_hooks';
export interface ExecutionContextConfig {
executionAsyncId: () => number;
createHook(config: {
init(asyncId: number, _: string, triggerAsyncId: number): void;
destroy(asyncId: number): void;
}): void;
}

export interface ExecutionContextPicker {
getModuleContext(moduleId: string): GraphQLModules.ModuleContext;
Expand All @@ -8,26 +14,7 @@ export interface ExecutionContextPicker {
const executionContextStore = new Map<number, ExecutionContextPicker>();
const executionContextDependencyStore = new Map<number, Set<number>>();

const executionContextHook = createHook({
init(asyncId, _, triggerAsyncId) {
// Store same context data for child async resources
const ctx = executionContextStore.get(triggerAsyncId);
if (ctx) {
const dependencies =
executionContextDependencyStore.get(triggerAsyncId) ??
executionContextDependencyStore
.set(triggerAsyncId, new Set())
.get(triggerAsyncId)!;
dependencies.add(asyncId);
executionContextStore.set(asyncId, ctx);
}
},
destroy(asyncId) {
if (executionContextStore.has(asyncId)) {
executionContextStore.delete(asyncId);
}
},
});
let executionAsyncId: () => number = () => 0;

function destroyContextAndItsChildren(id: number) {
if (executionContextStore.has(id)) {
Expand All @@ -44,33 +31,72 @@ function destroyContextAndItsChildren(id: number) {
}
}

let executionContextEnabled = false;

export const executionContext: {
create(picker: ExecutionContextPicker): () => void;
getModuleContext: ExecutionContextPicker['getModuleContext'];
getApplicationContext: ExecutionContextPicker['getApplicationContext'];
} = {
create(picker) {
if (!executionContextEnabled) {
return function destroyContextNoop() {
// noop
};
}

const id = executionAsyncId();
executionContextStore.set(id, picker);
return function destroyContext() {
destroyContextAndItsChildren(id);
};
},
getModuleContext(moduleId) {
assertExecutionContext();

const picker = executionContextStore.get(executionAsyncId())!;
return picker.getModuleContext(moduleId);
},
getApplicationContext() {
assertExecutionContext();

const picker = executionContextStore.get(executionAsyncId())!;
return picker.getApplicationContext();
},
};

let executionContextEnabled = false;
export function enableExecutionContext(config: ExecutionContextConfig) {
if (!executionContextEnabled) {
config.createHook({
init(asyncId, _, triggerAsyncId) {
// Store same context data for child async resources
const ctx = executionContextStore.get(triggerAsyncId);
if (ctx) {
const dependencies =
executionContextDependencyStore.get(triggerAsyncId) ??
executionContextDependencyStore
.set(triggerAsyncId, new Set())
.get(triggerAsyncId)!;
dependencies.add(asyncId);
executionContextStore.set(asyncId, ctx);
}
},
destroy(asyncId) {
if (executionContextStore.has(asyncId)) {
executionContextStore.delete(asyncId);
}
},
});
executionAsyncId = config.executionAsyncId;
executionContextEnabled = true;
}
}

export function enableExecutionContext() {
export function assertExecutionContext(): void | never {
if (!executionContextEnabled) {
executionContextHook.enable();
throw new Error(
'Execution Context is not enabled. Please set `executionContext` option in `createApplication`'
);
}
}

Expand Down
17 changes: 17 additions & 0 deletions packages/graphql-modules/src/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import type { Provider, Injector } from '../di';
import type { Resolvers, Module, MockedModule } from '../module/types';
import type { MiddlewareMap } from '../shared/middleware';
import type { ExecutionContextConfig } from './execution-context';
import type { ApolloRequestContext } from './apollo';
import type { Single } from '../shared/types';
import type { InternalAppContext } from './application';
Expand Down Expand Up @@ -139,4 +140,20 @@ export interface ApplicationConfig {
typeDefs: DocumentNode[];
resolvers: Record<string, any>[];
}): GraphQLSchema;

/**
* Enables ExecutionContext
*
* @example
*
* ```typescript
* import { createHook, executionAsyncId } from 'async_hooks';
*
* const app = createApplication({
* modules: [],
* executionContext: { createHook, executionAsyncId }
* });
* ```
*/
executionContext: ExecutionContextConfig | false;
}
2 changes: 0 additions & 2 deletions packages/graphql-modules/src/di/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
ensureInjectableMetadata,
} from './metadata';
import { Injector } from './injector';
import { enableExecutionContext } from '../application/execution-context';

function ensureReflect() {
if (!(Reflect && Reflect.getOwnMetadata)) {
Expand All @@ -17,7 +16,6 @@ function ensureReflect() {
export function Injectable(options?: ProviderOptions): ClassDecorator {
return (target) => {
ensureReflect();
enableExecutionContext();

const params: Type<any>[] = (
Reflect.getMetadata('design:paramtypes', target) || []
Expand Down
1 change: 1 addition & 0 deletions packages/graphql-modules/src/testing/test-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export function testModule(testedModule: Module, config?: TestModuleConfig) {
modules,
providers: config?.providers,
middlewares: config?.middlewares,
executionContext: false,
});
}

Expand Down
7 changes: 7 additions & 0 deletions packages/graphql-modules/tests/bootstrap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ test('fail when modules have non-unique ids', async () => {
expect(() => {
createApplication({
modules: [modFoo, modBar],
executionContext: false,
});
}).toThrow(`Modules with non-unique ids: foo`);
});
Expand Down Expand Up @@ -57,6 +58,7 @@ test('should allow multiple type extensions in the same module', async () => {

const app = createApplication({
modules: [m1],
executionContext: false,
});

const schema = app.schema;
Expand Down Expand Up @@ -97,6 +99,7 @@ test('should not thrown when isTypeOf is used', async () => {

const app = createApplication({
modules: [m1],
executionContext: false,
});

const result = await testkit.execute(app, {
Expand Down Expand Up @@ -152,6 +155,7 @@ test('should allow to add __isTypeOf to type resolvers', () => {
expect(() => {
createApplication({
modules: [m1],
executionContext: false,
});
}).not.toThrow();
});
Expand Down Expand Up @@ -209,6 +213,7 @@ test('should support __resolveType', async () => {

const app = createApplication({
modules: [m1],
executionContext: false,
});

const result = await testkit.execute(app, {
Expand Down Expand Up @@ -296,6 +301,7 @@ test('allow field resolvers in an interface without objects inheriting them', as

const app = createApplication({
modules: [mod],
executionContext: false,
});

const result = await testkit.execute(app, {
Expand Down Expand Up @@ -362,6 +368,7 @@ test('pass field resolvers of an interface to schemaBuilder', async () => {
inheritResolversFromInterfaces: true,
});
},
executionContext: false,
});

const result = await testkit.execute(app, {
Expand Down
1 change: 1 addition & 0 deletions packages/graphql-modules/tests/context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ test('Global context and module context should be reachable', async () => {

const app = createApplication({
modules: [postsModule],
executionContext: false,
});

const contextValue = () => ({
Expand Down
7 changes: 6 additions & 1 deletion packages/graphql-modules/tests/di-errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ test('No error in case of module without providers', async () => {
const app = createApplication({
modules: [mod],
providers: [Data],
executionContext: false,
});

const contextValue = { request: {}, response: {} };
Expand Down Expand Up @@ -238,7 +239,9 @@ test('Make sure we have readable error', async () => {
},
});

expect(() => createApplication({ modules: [m2, m1] })).toThrowError(
expect(() =>
createApplication({ modules: [m2, m1], executionContext: false })
).toThrowError(
'No provider for P1! (P2 -> P1) - in Module "m2" (Singleton Scope)'
);
});
Expand Down Expand Up @@ -311,6 +314,7 @@ test('Detect collision of two identical global providers (singleton)', async ()
createApplication({
modules: [fooModule, barModule],
providers: [AppData],
executionContext: false,
});
}).toThrowError(
`Failed to define 'Data' token as global. Token provided by two modules: 'bar', 'foo'`
Expand Down Expand Up @@ -385,6 +389,7 @@ test('Detect collision of two identical global providers (operation)', async ()
createApplication({
modules: [fooModule, barModule],
providers: [AppData],
executionContext: false,
});
}).toThrowError(
`Failed to define 'Data' token as global. Token provided by two modules: 'bar', 'foo'`
Expand Down
2 changes: 2 additions & 0 deletions packages/graphql-modules/tests/di-hooks.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'reflect-metadata';
import { createHook, executionAsyncId } from 'async_hooks';
import {
createApplication,
createModule,
Expand Down Expand Up @@ -79,6 +80,7 @@ test('OnDestroy hook', async () => {

const app = createApplication({
modules: [postsModule],
executionContext: { createHook, executionAsyncId },
});

const createContext = () => ({ request: {}, response: {} });
Expand Down
Loading