Skip to content

Commit eb3ad15

Browse files
kamilkisieladotansimha
authored andcommitted
YES
1 parent e47d3fa commit eb3ad15

File tree

10 files changed

+764
-102
lines changed

10 files changed

+764
-102
lines changed

.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
**/*.spec.ts
2+
**/dist

.eslintrc.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
env:
2+
browser: true
3+
es2021: true
4+
parser: '@typescript-eslint/parser'
5+
parserOptions:
6+
ecmaVersion: 12
7+
sourceType: module
8+
plugins:
9+
- '@typescript-eslint'
10+
rules:
11+
no-debugger: error
12+
no-console: error
13+
no-await-in-loop: error
14+
getter-return: error
15+
no-func-assign: error
16+
no-unsafe-negation: error
17+
no-unreachable: error

package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"release": "changeset publish",
1515
"release:canary": "(node scripts/canary-release.js && yarn build && yarn changeset publish --tag alpha) || echo Skipping Canary...",
1616
"format": "prettier --write \"packages/**/*.{js,json,css,md,ts,tsx}\"",
17+
"lint": "eslint \"packages/**\"",
1718
"benchmark:basic": "NODE_ENV=production ts-node --project tsconfig.app.json benchmark/basic.case.ts",
1819
"deploy-website": "cd website && yarn && yarn build && mkdir graphql-modules && mv build/* graphql-modules && mv graphql-modules build"
1920
},
@@ -24,20 +25,23 @@
2425
"@types/jest": "26.0.14",
2526
"@types/node": "14.11.1",
2627
"@types/ramda": "0.27.17",
28+
"@typescript-eslint/eslint-plugin": "4.1.1",
29+
"@typescript-eslint/parser": "4.1.1",
2730
"apollo-server": "2.17.0",
2831
"apollo-server-express": "2.17.0",
2932
"artillery": "1.6.1",
3033
"benchmark": "2.1.4",
3134
"bob-the-bundler": "1.1.0",
3235
"dataloader": "2.0.0",
36+
"eslint": "7.9.0",
3337
"express": "4.17.1",
3438
"express-graphql": "0.11.0",
3539
"graphql": "15.3.0",
3640
"graphql-subscriptions": "1.1.0",
3741
"husky": "4.3.0",
3842
"jest": "26.4.2",
43+
"lint-staged": "10.4.0",
3944
"prettier": "2.1.2",
40-
"pretty-quick": "3.0.2",
4145
"reflect-metadata": "0.1.13",
4246
"subscriptions-transport-ws": "0.9.18",
4347
"ts-jest": "26.4.0",
@@ -51,7 +55,11 @@
5155
},
5256
"husky": {
5357
"hooks": {
54-
"pre-commit": "pretty-quick --staged"
58+
"pre-commit": "lint-staged"
5559
}
60+
},
61+
"lint-staged": {
62+
"*.ts": "eslint",
63+
"*{js,json,css,md,ts,tsx}": "prettier --write"
5664
}
5765
}

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

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import tapAsyncIterator, {
2727
} from '../shared/utils';
2828
import { CONTEXT } from './tokens';
2929
import { ApplicationConfig, Application } from './types';
30-
import { GlobalProviderMap, ResolvedProvider } from '../di/resolution';
3130

3231
type ExecutionContextBuilder<
3332
TContext extends {
@@ -104,29 +103,29 @@ export function createApplication(config: ApplicationConfig): Application {
104103
);
105104
const moduleMap = createModuleMap(modules);
106105

107-
const singletonGlobalProviders: GlobalProviderMap = new Map();
108-
109-
function addToMap(
110-
provider: ResolvedProvider,
111-
registry: GlobalProviderMap,
112-
injector: ReflectiveInjector
113-
) {
114-
if (provider.factory.isGlobal) {
115-
if (registry.has(provider.key.id)) {
116-
throw new Error('Collision');
117-
}
118-
119-
registry.set(provider.key.id, injector);
120-
}
121-
}
106+
const singletonGlobalProvidersMap: {
107+
/**
108+
* Provider key -> Module ID
109+
*/
110+
[key: string]: string;
111+
} = {};
122112

123113
modules.forEach((mod) => {
124114
mod.singletonProviders.forEach((provider) => {
125-
addToMap(provider, singletonGlobalProviders, mod.injector);
115+
if (provider.factory.isGlobal) {
116+
singletonGlobalProvidersMap[provider.key.id] = mod.id;
117+
}
126118
});
127119
});
128120

129-
appInjector._globalProvidersMap = singletonGlobalProviders;
121+
appInjector._globalProvidersMap = {
122+
has(key) {
123+
return typeof singletonGlobalProvidersMap[key] === 'string';
124+
},
125+
get(key) {
126+
return moduleMap.get(singletonGlobalProvidersMap[key])!.injector;
127+
},
128+
};
130129

131130
// Creating a schema, flattening the typedefs and resolvers
132131
// is not expensive since it happens only once
@@ -167,6 +166,29 @@ export function createApplication(config: ApplicationConfig): Application {
167166
() => appContext
168167
);
169168

169+
// It's very important to recreate a Singleton Injector
170+
// and add an execution context getter function
171+
// We do this so Singleton provider can access the ExecutionContext via Proxy
172+
const proxyModuleMap = new Map<string, ReflectiveInjector>();
173+
174+
moduleMap.forEach((mod, moduleId) => {
175+
const singletonModuleInjector = mod.injector;
176+
const singletonModuleProxyInjector = ReflectiveInjector.createWithExecutionContext(
177+
singletonModuleInjector,
178+
() => contextCache[moduleId]
179+
);
180+
proxyModuleMap.set(moduleId, singletonModuleProxyInjector);
181+
});
182+
183+
singletonAppProxyInjector._globalProvidersMap = {
184+
has(key) {
185+
return typeof singletonGlobalProvidersMap[key] === 'string';
186+
},
187+
get(key) {
188+
return proxyModuleMap.get(singletonGlobalProvidersMap[key])!;
189+
},
190+
};
191+
170192
// As the name of the Injector says, it's an Operation scoped Injector
171193
// Application level
172194
// Operation scoped - means it's created and destroyed on every GraphQL Operation
@@ -213,18 +235,6 @@ export function createApplication(config: ApplicationConfig): Application {
213235
if (!contextCache[moduleId]) {
214236
// We're interested in operation-scoped providers only
215237
const providers = moduleMap.get(moduleId)?.operationProviders!;
216-
// Module-level Singleton Injector
217-
const singletonModuleInjector = moduleMap.get(moduleId)!.injector;
218-
219-
(singletonModuleInjector as any)._parent = singletonAppProxyInjector;
220-
221-
// It's very important to recreate a Singleton Injector
222-
// and add an execution context getter function
223-
// We do this so Singleton provider can access the ExecutionContext via Proxy
224-
const singletonModuleProxyInjector = ReflectiveInjector.createWithExecutionContext(
225-
singletonModuleInjector,
226-
() => contextCache[moduleId]
227-
);
228238

229239
// Create module-level Operation-scoped Injector
230240
const operationModuleInjector = ReflectiveInjector.createFromResolved(
@@ -241,7 +251,7 @@ export function createApplication(config: ApplicationConfig): Application {
241251
])
242252
),
243253
// This injector has a priority
244-
parent: singletonModuleProxyInjector,
254+
parent: proxyModuleMap.get(moduleId),
245255
// over this one
246256
fallbackParent: operationAppInjector,
247257
}
@@ -257,6 +267,11 @@ export function createApplication(config: ApplicationConfig): Application {
257267
};
258268
}
259269

270+
// HEY HEY HEY: changing `parent` of singleton injector may be incorret
271+
// what if we get two operations and we're in the middle of two async actions?
272+
(moduleMap.get(moduleId)!
273+
.injector as any)._parent = singletonAppProxyInjector;
274+
260275
return contextCache[moduleId];
261276
},
262277
},

packages/graphql-modules/src/di/injector.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,6 @@ export class ReflectiveInjector implements Injector {
165165
}
166166

167167
get(token: any, notFoundValue: any = _THROW_IF_NOT_FOUND): any {
168-
debugger;
169168
return this._getByKey(Key.get(token), notFoundValue);
170169
}
171170

packages/graphql-modules/src/di/resolution.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ export type NormalizedProvider<T = any> =
2020

2121
const _EMPTY_LIST: any[] = [];
2222

23-
export type GlobalProviderMap = Map<Key['id'], ReflectiveInjector>;
23+
export type GlobalProviderMap = {
24+
has(key: Key['id']): boolean;
25+
get(key: Key['id']): ReflectiveInjector;
26+
};
2427

2528
export class ResolvedProvider {
2629
constructor(public key: Key, public factory: ResolvedFactory) {}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function getOriginalError(error: Error): Error {
1111
}
1212

1313
function defaultErrorLogger(console: Console, ...values: any[]) {
14+
// eslint-disable-next-line no-console
1415
(<any>console.error)(...values);
1516
}
1617

packages/graphql-modules/tests/index.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ test('basic', async () => {
125125
Events,
126126
{
127127
provide: Test,
128-
useValue: 'local',
128+
useValue: 'mod',
129129
},
130130
],
131131
typeDefs: gql`
@@ -198,7 +198,7 @@ test('basic', async () => {
198198
Logger,
199199
{
200200
provide: Test,
201-
useValue: 'global',
201+
useValue: 'app',
202202
},
203203
],
204204
});
@@ -235,8 +235,8 @@ test('basic', async () => {
235235
});
236236

237237
// Child Injector has priority over Parent Injector
238-
expect(spies.posts.test).toHaveBeenCalledWith('local');
239-
expect(spies.comments.test).toHaveBeenCalledWith('global');
238+
expect(spies.posts.test).toHaveBeenCalledWith('mod');
239+
expect(spies.comments.test).toHaveBeenCalledWith('app');
240240

241241
// Value of MODULE_ID according to module's resolver
242242
expect(spies.posts.moduleId).toHaveBeenCalledWith('posts');

packages/graphql-modules/tests/providers.spec.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
createModule,
55
Injectable,
66
Inject,
7+
InjectionToken,
78
CONTEXT,
89
Scope,
910
gql,
@@ -615,3 +616,119 @@ test('Global Token provided by one module should be accessible by other modules
615616
bar: 'ipsum',
616617
});
617618
});
619+
620+
test('Global Token (module) should use other local tokens (singleton)', async () => {
621+
const LogLevel = new InjectionToken<string>('log-level');
622+
const logger = jest.fn();
623+
624+
@Injectable({
625+
scope: Scope.Singleton,
626+
global: true,
627+
})
628+
class Data {
629+
constructor(@Inject(LogLevel) private logLevel: string) {}
630+
631+
lorem() {
632+
logger(this.logLevel);
633+
return 'ipsum';
634+
}
635+
}
636+
637+
@Injectable({
638+
scope: Scope.Singleton,
639+
})
640+
class AppData {
641+
constructor(private data: Data) {}
642+
643+
ispum() {
644+
return this.data.lorem();
645+
}
646+
}
647+
648+
const fooModule = createModule({
649+
id: 'foo',
650+
providers: [Data, { provide: LogLevel, useValue: 'info' }],
651+
typeDefs: gql`
652+
type Query {
653+
foo: String!
654+
}
655+
`,
656+
resolvers: {
657+
Query: {
658+
foo(
659+
_parent: {},
660+
_args: {},
661+
{ injector }: GraphQLModules.ModuleContext
662+
) {
663+
return injector.get(Data).lorem();
664+
},
665+
},
666+
},
667+
});
668+
669+
const barModule = createModule({
670+
id: 'bar',
671+
providers: [
672+
{
673+
provide: LogLevel,
674+
useValue: 'error',
675+
},
676+
],
677+
typeDefs: gql`
678+
extend type Query {
679+
bar: String!
680+
}
681+
`,
682+
resolvers: {
683+
Query: {
684+
bar(
685+
_parent: {},
686+
_args: {},
687+
{ injector }: GraphQLModules.ModuleContext
688+
) {
689+
return injector.get(Data).lorem();
690+
},
691+
},
692+
},
693+
});
694+
695+
const app = createApplication({
696+
modules: [fooModule, barModule],
697+
providers: [
698+
AppData,
699+
{
700+
provide: LogLevel,
701+
useValue: 'verbose',
702+
},
703+
],
704+
});
705+
706+
const schema = makeExecutableSchema({
707+
typeDefs: app.typeDefs,
708+
resolvers: app.resolvers,
709+
});
710+
711+
const contextValue = { request: {}, response: {} };
712+
const document = parse(/* GraphQL */ `
713+
{
714+
foo
715+
bar
716+
}
717+
`);
718+
719+
const result = await app.createExecution()({
720+
schema,
721+
contextValue,
722+
document,
723+
});
724+
725+
expect(result.errors).toBeUndefined();
726+
expect(result.data).toEqual({
727+
foo: 'ipsum',
728+
bar: 'ipsum',
729+
});
730+
731+
expect(logger).toHaveBeenCalledTimes(2);
732+
expect(logger).toHaveBeenNthCalledWith(1, 'info');
733+
expect(logger).toHaveBeenNthCalledWith(2, 'info');
734+
});

0 commit comments

Comments
 (0)