@tsed/apollo 8.0.0 → 8.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -9
- package/src/ApolloModule.ts +59 -0
- package/src/constants/constants.ts +2 -0
- package/src/decorators/dataSource.ts +70 -0
- package/src/decorators/injectApolloContext.ts +21 -0
- package/src/index.ts +13 -0
- package/src/interfaces/AlterApolloContext.ts +7 -0
- package/src/interfaces/AlterApolloServerPlugins.ts +10 -0
- package/src/interfaces/AlterApolloSettings.ts +10 -0
- package/src/interfaces/ApolloContext.ts +5 -0
- package/src/interfaces/ApolloSettings.ts +44 -0
- package/src/services/ApolloService.spec.ts +319 -0
- package/src/services/ApolloService.ts +230 -0
package/package.json
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
"name": "@tsed/apollo",
|
|
3
3
|
"description": "Apollo package for Ts.ED framework",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.0.
|
|
5
|
+
"version": "8.0.2",
|
|
6
6
|
"source": "./src/index.ts",
|
|
7
7
|
"main": "./lib/esm/index.js",
|
|
8
8
|
"module": "./lib/esm/index.js",
|
|
9
9
|
"typings": "./lib/types/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
+
"@tsed/source": "./src/index.ts",
|
|
12
13
|
"types": "./lib/types/index.d.ts",
|
|
13
14
|
"import": "./lib/esm/index.js",
|
|
14
15
|
"default": "./lib/esm/index.js"
|
|
@@ -28,12 +29,12 @@
|
|
|
28
29
|
"@apollo/datasource-rest": "6.3.0",
|
|
29
30
|
"@apollo/server": "^4.11.0",
|
|
30
31
|
"@as-integrations/koa": "1.1.1",
|
|
31
|
-
"@tsed/barrels": "8.0.
|
|
32
|
-
"@tsed/core": "8.0.
|
|
33
|
-
"@tsed/di": "8.0.
|
|
32
|
+
"@tsed/barrels": "8.0.2",
|
|
33
|
+
"@tsed/core": "8.0.2",
|
|
34
|
+
"@tsed/di": "8.0.2",
|
|
34
35
|
"@tsed/logger": "^6.7.8",
|
|
35
|
-
"@tsed/platform-http": "8.0.
|
|
36
|
-
"@tsed/typescript": "8.0.
|
|
36
|
+
"@tsed/platform-http": "8.0.2",
|
|
37
|
+
"@tsed/typescript": "8.0.2",
|
|
37
38
|
"apollo-datasource-http": "0.21.0",
|
|
38
39
|
"apollo-server-testing": "2.26.2",
|
|
39
40
|
"eslint": "9.12.0",
|
|
@@ -43,10 +44,10 @@
|
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
46
|
"@apollo/server": ">=4.10.4",
|
|
46
|
-
"@tsed/core": "8.0.
|
|
47
|
-
"@tsed/di": "8.0.
|
|
47
|
+
"@tsed/core": "8.0.2",
|
|
48
|
+
"@tsed/di": "8.0.2",
|
|
48
49
|
"@tsed/logger": ">=6.7.5",
|
|
49
|
-
"@tsed/platform-http": "8.0.
|
|
50
|
+
"@tsed/platform-http": "8.0.2",
|
|
50
51
|
"graphql": ">16.0.0"
|
|
51
52
|
},
|
|
52
53
|
"repository": "https://github.com/tsedio/tsed",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {Configuration, Inject, InjectorService, Module} from "@tsed/di";
|
|
2
|
+
import {Logger} from "@tsed/logger";
|
|
3
|
+
import {AfterListen, OnRoutesInit, PlatformConfiguration} from "@tsed/platform-http";
|
|
4
|
+
|
|
5
|
+
import {ApolloSettings} from "./interfaces/ApolloSettings.js";
|
|
6
|
+
import {ApolloService} from "./services/ApolloService.js";
|
|
7
|
+
|
|
8
|
+
@Module()
|
|
9
|
+
export class ApolloModule implements OnRoutesInit, AfterListen {
|
|
10
|
+
@Inject()
|
|
11
|
+
protected logger: Logger;
|
|
12
|
+
|
|
13
|
+
@Inject()
|
|
14
|
+
protected service: ApolloService;
|
|
15
|
+
|
|
16
|
+
@Configuration()
|
|
17
|
+
protected configuration: PlatformConfiguration;
|
|
18
|
+
|
|
19
|
+
@Inject(InjectorService)
|
|
20
|
+
protected injector: InjectorService;
|
|
21
|
+
|
|
22
|
+
get settings(): {[key: string]: ApolloSettings} | undefined {
|
|
23
|
+
return this.configuration.get("apollo", this.configuration.get("graphql", this.configuration.get("typegraphql")));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async $onRoutesInit(): Promise<void> {
|
|
27
|
+
const {settings, injector} = this;
|
|
28
|
+
|
|
29
|
+
if (settings) {
|
|
30
|
+
const promises = Object.entries(settings).map(async ([id, options]) => {
|
|
31
|
+
options = await injector.alterAsync("$alterApolloSettings", {id, ...options});
|
|
32
|
+
|
|
33
|
+
return this.service.createServer(id, options);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await Promise.all(promises);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
$afterListen(): Promise<any> | void {
|
|
41
|
+
const host = this.configuration.getBestHost();
|
|
42
|
+
|
|
43
|
+
const displayLog = (key: string, path: string) => {
|
|
44
|
+
const url = "port" in host && typeof host.port === "number" ? `${host.protocol}://${host.address}:${host.port}` : "";
|
|
45
|
+
|
|
46
|
+
this.logger.info(`[${key}] Apollo server is available on ${url}/${path.replace(/^\//, "")}`);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const {settings} = this;
|
|
50
|
+
|
|
51
|
+
if (settings) {
|
|
52
|
+
Object.entries(settings).map(([key, options]) => {
|
|
53
|
+
const {path} = options;
|
|
54
|
+
|
|
55
|
+
displayLog(key, path);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {StoreSet, useDecorators} from "@tsed/core";
|
|
2
|
+
import {Injectable, ProviderScope} from "@tsed/di";
|
|
3
|
+
|
|
4
|
+
import {DATASOURCES_PROVIDERS} from "../constants/constants.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a new injectable DataSource.
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { DataSourceService } from "@tsed/graphql";
|
|
11
|
+
* import { RESTDataSource } from '@apollo/datasource-rest';
|
|
12
|
+
*
|
|
13
|
+
* @DataSource()
|
|
14
|
+
* export class MyDataSource extends RESTDataSource {
|
|
15
|
+
* constructor() {
|
|
16
|
+
* super();
|
|
17
|
+
* this.baseURL = 'https://awesome-api.example.com';
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* willSendRequest(request) {
|
|
21
|
+
* request.headers.set('Authorization', this.context.token);
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* getMyData(id: string) {
|
|
25
|
+
* return this.get(`/v1/my_rest_data/${id}`);
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @decorator
|
|
31
|
+
* @graphql
|
|
32
|
+
*/
|
|
33
|
+
export function DataSource(name?: string): ClassDecorator {
|
|
34
|
+
return useDecorators(
|
|
35
|
+
Injectable({
|
|
36
|
+
type: DATASOURCES_PROVIDERS,
|
|
37
|
+
scope: ProviderScope.INSTANCE
|
|
38
|
+
}),
|
|
39
|
+
StoreSet(DATASOURCES_PROVIDERS, {name})
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a new injectable DataSource.
|
|
45
|
+
*
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { DataSourceService } from "@tsed/graphql";
|
|
48
|
+
* import { RESTDataSource } from '@apollo/datasource-rest';
|
|
49
|
+
*
|
|
50
|
+
* @DataSource()
|
|
51
|
+
* export class MyDataSource extends RESTDataSource {
|
|
52
|
+
* constructor() {
|
|
53
|
+
* super();
|
|
54
|
+
* this.baseURL = 'https://awesome-api.example.com';
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* willSendRequest(request) {
|
|
58
|
+
* request.headers.set('Authorization', this.context.token);
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* getMyData(id: string) {
|
|
62
|
+
* return this.get(`/v1/my_rest_data/${id}`);
|
|
63
|
+
* }
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @decorator
|
|
68
|
+
* @graphql
|
|
69
|
+
*/
|
|
70
|
+
export const DataSourceService = DataSource;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {DecoratorParameters, decoratorTypeOf, DecoratorTypes} from "@tsed/core";
|
|
2
|
+
import {Inject, InjectContext} from "@tsed/di";
|
|
3
|
+
|
|
4
|
+
import {APOLLO_CONTEXT} from "../constants/constants.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Inject the Apollo context in the decorated property.
|
|
8
|
+
* @decorator
|
|
9
|
+
*/
|
|
10
|
+
export function InjectApolloContext(): any {
|
|
11
|
+
return (...args: DecoratorParameters) => {
|
|
12
|
+
switch (decoratorTypeOf(args)) {
|
|
13
|
+
case DecoratorTypes.PARAM_CTOR:
|
|
14
|
+
Inject(APOLLO_CONTEXT)(...args);
|
|
15
|
+
break;
|
|
16
|
+
|
|
17
|
+
case DecoratorTypes.PROP:
|
|
18
|
+
return InjectContext(($ctx) => $ctx.get(APOLLO_CONTEXT))(args[0], args[1]);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Automatically generated by @tsed/barrels.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./ApolloModule.js";
|
|
5
|
+
export * from "./constants/constants.js";
|
|
6
|
+
export * from "./decorators/dataSource.js";
|
|
7
|
+
export * from "./decorators/injectApolloContext.js";
|
|
8
|
+
export * from "./interfaces/AlterApolloContext.js";
|
|
9
|
+
export * from "./interfaces/AlterApolloServerPlugins.js";
|
|
10
|
+
export * from "./interfaces/AlterApolloSettings.js";
|
|
11
|
+
export * from "./interfaces/ApolloContext.js";
|
|
12
|
+
export * from "./interfaces/ApolloSettings.js";
|
|
13
|
+
export * from "./services/ApolloService.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type {PlatformContext} from "@tsed/platform-http";
|
|
2
|
+
|
|
3
|
+
import type {ApolloContext} from "./ApolloContext.js";
|
|
4
|
+
|
|
5
|
+
export interface AlterApolloContext<Context extends ApolloContext = ApolloContext> {
|
|
6
|
+
$alterApolloContext(context: Context, $ctx: PlatformContext): Context | Promise<Context>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type {ApolloServerPlugin} from "@apollo/server";
|
|
2
|
+
|
|
3
|
+
import type {ApolloSettings} from "./ApolloSettings.js";
|
|
4
|
+
|
|
5
|
+
export interface AlterApolloServerPlugins {
|
|
6
|
+
$alterApolloServerPlugins(
|
|
7
|
+
plugins: ApolloServerPlugin[],
|
|
8
|
+
serverSettings: ApolloSettings
|
|
9
|
+
): ApolloServerPlugin[] | Promise<ApolloServerPlugin[]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type {ApolloContext} from "./ApolloContext.js";
|
|
2
|
+
import type {ApolloSettingsWithID} from "./ApolloSettings.js";
|
|
3
|
+
|
|
4
|
+
export interface AlterApolloSettings<Context extends ApolloContext = ApolloContext> {
|
|
5
|
+
$alterApolloSettings(settings: ApolloSettingsWithID<Context>):
|
|
6
|
+
| (ApolloSettingsWithID<Context> & {
|
|
7
|
+
id: string;
|
|
8
|
+
})
|
|
9
|
+
| Promise<ApolloSettingsWithID<Context>>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type {ApolloServer, ApolloServerOptions, ApolloServerOptionsWithSchema} from "@apollo/server";
|
|
2
|
+
import type {GatewayInterface} from "@apollo/server-gateway-interface";
|
|
3
|
+
import type {IExecutableSchemaDefinition} from "@graphql-tools/schema";
|
|
4
|
+
import type {GraphQLSchema} from "graphql/index.js";
|
|
5
|
+
|
|
6
|
+
import type {ApolloContext} from "./ApolloContext.js";
|
|
7
|
+
|
|
8
|
+
export type ApolloMiddlewareOptions = Record<string, any>;
|
|
9
|
+
export type ApolloCustomServerCB<TContext extends ApolloContext> = (config: ApolloServerOptions<TContext>) => ApolloServer<TContext>;
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
namespace TsED {
|
|
13
|
+
interface ApolloSettings {}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ApolloServerOptionsBase<TContext extends ApolloContext = ApolloContext> = Omit<
|
|
18
|
+
ApolloServerOptionsWithSchema<TContext>,
|
|
19
|
+
"schema" | "typeDefs" | "resolvers" | "gateway"
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
export type ApolloSettings<TContext extends ApolloContext = ApolloContext> = ApolloServerOptionsBase<TContext> &
|
|
23
|
+
TsED.ApolloSettings & {
|
|
24
|
+
// Basic options
|
|
25
|
+
path: string;
|
|
26
|
+
server?: ApolloCustomServerCB<TContext>;
|
|
27
|
+
playground?: boolean;
|
|
28
|
+
// ApplyMiddleware Options
|
|
29
|
+
// See options descriptions on https://www.apollographql.com/docs/apollo-server/api/apollo-server.html
|
|
30
|
+
serverRegistration?: ApolloMiddlewareOptions;
|
|
31
|
+
middlewareOptions?: ApolloMiddlewareOptions;
|
|
32
|
+
schema?: GraphQLSchema;
|
|
33
|
+
typeDefs?: IExecutableSchemaDefinition<TContext>["typeDefs"];
|
|
34
|
+
resolvers?: IExecutableSchemaDefinition<TContext>["resolvers"];
|
|
35
|
+
gateway?: GatewayInterface;
|
|
36
|
+
/**
|
|
37
|
+
* @deprecated use $alterApolloContext instead
|
|
38
|
+
*/
|
|
39
|
+
dataSources?: () => Record<string, unknown>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type ApolloSettingsWithID<TContext extends ApolloContext = ApolloContext> = ApolloSettings<TContext> & {
|
|
43
|
+
id: string;
|
|
44
|
+
};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import {RESTDataSource} from "@apollo/datasource-rest";
|
|
2
|
+
import {ApolloServer, ApolloServerPlugin} from "@apollo/server";
|
|
3
|
+
import {catchAsyncError} from "@tsed/core";
|
|
4
|
+
import {Configuration, Constant, InjectContext, Module, runInContext} from "@tsed/di";
|
|
5
|
+
import {Logger} from "@tsed/logger";
|
|
6
|
+
import {PlatformApplication, PlatformContext} from "@tsed/platform-http";
|
|
7
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
8
|
+
import {HTTPDataSource} from "apollo-datasource-http";
|
|
9
|
+
|
|
10
|
+
import {APOLLO_CONTEXT} from "../constants/constants.js";
|
|
11
|
+
import {DataSource} from "../decorators/dataSource.js";
|
|
12
|
+
import {InjectApolloContext} from "../decorators/injectApolloContext.js";
|
|
13
|
+
import type {AlterApolloContext} from "../interfaces/AlterApolloContext.js";
|
|
14
|
+
import type {AlterApolloServerPlugins} from "../interfaces/AlterApolloServerPlugins.js";
|
|
15
|
+
import type {ApolloContext} from "../interfaces/ApolloContext.js";
|
|
16
|
+
import type {ApolloSettings} from "../interfaces/ApolloSettings.js";
|
|
17
|
+
import {ApolloService} from "./ApolloService.js";
|
|
18
|
+
|
|
19
|
+
vi.mock("@apollo/server/express4", () => {
|
|
20
|
+
return {
|
|
21
|
+
__esModule: true,
|
|
22
|
+
expressMiddleware: vi.fn().mockReturnValue("expressMiddleware")
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
vi.mock("@as-integrations/koa", () => {
|
|
26
|
+
return {
|
|
27
|
+
__esModule: true,
|
|
28
|
+
koaMiddleware: vi.fn().mockReturnValue("koaMiddleware")
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export interface CustomApolloContext extends ApolloContext {
|
|
33
|
+
token: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Module()
|
|
37
|
+
class MyModule implements AlterApolloContext, AlterApolloServerPlugins {
|
|
38
|
+
$alterApolloServerPlugins(
|
|
39
|
+
plugins: ApolloServerPlugin[],
|
|
40
|
+
serverSettings: ApolloSettings
|
|
41
|
+
): ApolloServerPlugin[] | Promise<ApolloServerPlugin[]> {
|
|
42
|
+
return plugins.concat("extraPlugin" as never);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
$alterApolloContext(context: ApolloContext, $ctx: PlatformContext): CustomApolloContext | Promise<CustomApolloContext> {
|
|
46
|
+
const header = $ctx.request.get("authorization");
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...context,
|
|
50
|
+
token: header
|
|
51
|
+
} as CustomApolloContext;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@DataSource()
|
|
56
|
+
export class MyDataSource extends RESTDataSource {
|
|
57
|
+
@InjectContext()
|
|
58
|
+
$ctx: PlatformContext;
|
|
59
|
+
|
|
60
|
+
@Constant("envs.MY_BACK_URL", "http://localhost:8001")
|
|
61
|
+
declare baseURL: string;
|
|
62
|
+
|
|
63
|
+
@InjectApolloContext()
|
|
64
|
+
protected context: CustomApolloContext;
|
|
65
|
+
|
|
66
|
+
constructor(server: ApolloServer, logger: Logger) {
|
|
67
|
+
super({
|
|
68
|
+
logger,
|
|
69
|
+
cache: server.cache
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
willSendRequest(path: string, request: any) {
|
|
74
|
+
request.headers["authorization"] = this.context.token;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getMyData(id: string) {
|
|
78
|
+
return this.get(`/rest/calendars/${id}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@DataSource("myName")
|
|
83
|
+
export class MyDataSource2 extends RESTDataSource {
|
|
84
|
+
@InjectContext()
|
|
85
|
+
$ctx: PlatformContext;
|
|
86
|
+
|
|
87
|
+
constructor() {
|
|
88
|
+
super();
|
|
89
|
+
this.baseURL = "http://localhost:8001";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getMyData(id: string) {
|
|
93
|
+
return this.get(`/rest/calendars/${id}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@DataSource()
|
|
98
|
+
class MoviesAPI extends HTTPDataSource {
|
|
99
|
+
constructor(
|
|
100
|
+
@InjectApolloContext() {token}: CustomApolloContext,
|
|
101
|
+
server: ApolloServer,
|
|
102
|
+
logger: Logger,
|
|
103
|
+
@Configuration() configuration: Configuration
|
|
104
|
+
) {
|
|
105
|
+
// the necessary arguments for HTTPDataSource
|
|
106
|
+
super(configuration.get("envs.MOVIES_API_URL"), {
|
|
107
|
+
logger
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// We need to call the initialize method in our data source's
|
|
111
|
+
// constructor, passing in our cache and contextValue.
|
|
112
|
+
this.initialize({cache: server.cache, context: token});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getMovie(id: string) {
|
|
116
|
+
return this.get(`movies/${encodeURIComponent(id)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getFixture() {
|
|
121
|
+
const service = PlatformTest.get<ApolloService>(ApolloService);
|
|
122
|
+
const logger = PlatformTest.get<Logger>(Logger);
|
|
123
|
+
const app = PlatformTest.get(PlatformApplication);
|
|
124
|
+
|
|
125
|
+
const serverMockInstance = {
|
|
126
|
+
start: vi.fn()
|
|
127
|
+
};
|
|
128
|
+
const serverMock = vi.fn().mockReturnValue(serverMockInstance);
|
|
129
|
+
|
|
130
|
+
vi.spyOn(app, "use").mockReturnThis();
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
service,
|
|
134
|
+
logger,
|
|
135
|
+
app,
|
|
136
|
+
serverMock,
|
|
137
|
+
serverMockInstance
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
describe("ApolloService", () => {
|
|
142
|
+
describe("when platform is express", () => {
|
|
143
|
+
beforeEach(() =>
|
|
144
|
+
PlatformTest.create({
|
|
145
|
+
PLATFORM_NAME: "express",
|
|
146
|
+
envs: {
|
|
147
|
+
MOVIES_API_URL: "http://localhost:8001"
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
afterEach(() => {
|
|
152
|
+
return PlatformTest.reset();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("createServer()", () => {
|
|
156
|
+
it("should create a server", async () => {
|
|
157
|
+
// GIVEN
|
|
158
|
+
const {serverMock, service, app} = getFixture();
|
|
159
|
+
|
|
160
|
+
const opts = {
|
|
161
|
+
path: "/path",
|
|
162
|
+
server: serverMock,
|
|
163
|
+
schema: "schema",
|
|
164
|
+
typeDefs: "typeDefs",
|
|
165
|
+
resolvers: "resolvers"
|
|
166
|
+
} as never;
|
|
167
|
+
|
|
168
|
+
// WHEN
|
|
169
|
+
const result1 = await service.createServer("key", opts);
|
|
170
|
+
const result2 = await service.createServer("key", opts);
|
|
171
|
+
|
|
172
|
+
expect(service.getSchema("key")).toEqual("schema");
|
|
173
|
+
expect(service.getSchema()).toEqual(undefined);
|
|
174
|
+
expect(service.getResolvers("key")).toEqual("resolvers");
|
|
175
|
+
expect(service.getResolvers()).toEqual(undefined);
|
|
176
|
+
expect(service.getTypeDefs("key")).toEqual("typeDefs");
|
|
177
|
+
expect(service.getTypeDefs()).toEqual(undefined);
|
|
178
|
+
expect(service.has("key")).toEqual(true);
|
|
179
|
+
expect(service.has()).toEqual(false);
|
|
180
|
+
expect(result2).toEqual(result1);
|
|
181
|
+
expect(app.use).toHaveBeenCalledWith("/path", "expressMiddleware");
|
|
182
|
+
expect(serverMock).toHaveBeenCalledWith({
|
|
183
|
+
plugins: expect.any(Array),
|
|
184
|
+
schema: "schema",
|
|
185
|
+
typeDefs: "typeDefs",
|
|
186
|
+
resolvers: "resolvers"
|
|
187
|
+
});
|
|
188
|
+
expect(serverMock.mock.calls[0][0].plugins).toContain("extraPlugin");
|
|
189
|
+
});
|
|
190
|
+
it("should log server error", async () => {
|
|
191
|
+
// GIVEN
|
|
192
|
+
const {serverMock, logger, serverMockInstance, service, app} = getFixture();
|
|
193
|
+
|
|
194
|
+
const opts = {
|
|
195
|
+
path: "/path",
|
|
196
|
+
server: serverMock
|
|
197
|
+
} as never;
|
|
198
|
+
|
|
199
|
+
vi.spyOn(logger, "error");
|
|
200
|
+
|
|
201
|
+
serverMockInstance.start.mockRejectedValue(new Error("test"));
|
|
202
|
+
|
|
203
|
+
// WHEN
|
|
204
|
+
const result = await catchAsyncError(() => service.createServer("key", opts));
|
|
205
|
+
|
|
206
|
+
expect(result).toEqual(new Error("test"));
|
|
207
|
+
expect(logger.error).toHaveBeenCalledWith({
|
|
208
|
+
error_name: "Error",
|
|
209
|
+
event: "APOLLO_BOOTSTRAP_ERROR",
|
|
210
|
+
message: "test",
|
|
211
|
+
stack: expect.any(String)
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("createContextHandler()", () => {
|
|
217
|
+
it("should create a context handler", async () => {
|
|
218
|
+
// GIVEN
|
|
219
|
+
const {service, serverMock} = getFixture();
|
|
220
|
+
const $ctx = PlatformTest.createRequestContext();
|
|
221
|
+
|
|
222
|
+
const opts = {
|
|
223
|
+
path: "/path",
|
|
224
|
+
server: serverMock
|
|
225
|
+
} as never;
|
|
226
|
+
|
|
227
|
+
// WHEN
|
|
228
|
+
const server = await service.createServer("key", opts);
|
|
229
|
+
|
|
230
|
+
// WHEN
|
|
231
|
+
const result = service.createContextHandler(server, {
|
|
232
|
+
dataSources: () => ({
|
|
233
|
+
myDataSource: MyDataSource
|
|
234
|
+
})
|
|
235
|
+
} as never);
|
|
236
|
+
|
|
237
|
+
$ctx.request.headers["authorization"] = "token";
|
|
238
|
+
|
|
239
|
+
const contextResult: CustomApolloContext = await runInContext($ctx, () => result());
|
|
240
|
+
|
|
241
|
+
expect(contextResult.token).toEqual("token");
|
|
242
|
+
expect(Object.keys(contextResult.dataSources)).toEqual(["myDataSource", "myName", "moviesAPI"]);
|
|
243
|
+
|
|
244
|
+
expect($ctx.get(APOLLO_CONTEXT)).toEqual(contextResult);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
describe("when platform is koa", () => {
|
|
249
|
+
beforeEach(() =>
|
|
250
|
+
PlatformTest.create({
|
|
251
|
+
PLATFORM_NAME: "koa"
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
afterEach(() => {
|
|
255
|
+
return PlatformTest.reset();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("createServer()", () => {
|
|
259
|
+
it("should create a server", async () => {
|
|
260
|
+
// GIVEN
|
|
261
|
+
const {serverMock, app, service} = getFixture();
|
|
262
|
+
|
|
263
|
+
// WHEN
|
|
264
|
+
const opts = {
|
|
265
|
+
path: "/path",
|
|
266
|
+
server: serverMock
|
|
267
|
+
} as never;
|
|
268
|
+
|
|
269
|
+
const result1 = await service.createServer("key", opts);
|
|
270
|
+
const result2 = await service.createServer("key", {path: "/path"} as any);
|
|
271
|
+
|
|
272
|
+
expect(service.getSchema("key")).toEqual(undefined);
|
|
273
|
+
expect(service.getSchema()).toEqual(undefined);
|
|
274
|
+
expect(result2).toEqual(result1);
|
|
275
|
+
expect(app.use).toHaveBeenCalledWith("/path", "koaMiddleware");
|
|
276
|
+
expect(serverMock).toHaveBeenCalledWith({
|
|
277
|
+
plugins: expect.any(Array)
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
describe("when platform is unknown", () => {
|
|
283
|
+
beforeEach(() =>
|
|
284
|
+
PlatformTest.create({
|
|
285
|
+
PLATFORM_NAME: "unknown"
|
|
286
|
+
})
|
|
287
|
+
);
|
|
288
|
+
afterEach(() => {
|
|
289
|
+
return PlatformTest.reset();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("createServer()", () => {
|
|
293
|
+
it("should not create a server", async () => {
|
|
294
|
+
// GIVEN
|
|
295
|
+
const {serverMock, logger, service} = getFixture();
|
|
296
|
+
|
|
297
|
+
vi.spyOn(logger, "warn");
|
|
298
|
+
// WHEN
|
|
299
|
+
const opts = {
|
|
300
|
+
path: "/path",
|
|
301
|
+
server: serverMock
|
|
302
|
+
} as never;
|
|
303
|
+
|
|
304
|
+
// WHEN
|
|
305
|
+
const result1 = await service.createServer("key", opts);
|
|
306
|
+
const result2 = await service.createServer("key", opts);
|
|
307
|
+
|
|
308
|
+
expect(service.getSchema("key")).toEqual(undefined);
|
|
309
|
+
expect(service.getSchema("key")).toEqual(undefined);
|
|
310
|
+
expect(service.getSchema()).toEqual(undefined);
|
|
311
|
+
expect(result2).toEqual(result1);
|
|
312
|
+
expect(logger.warn).toHaveBeenCalledWith({
|
|
313
|
+
event: "APOLLO_UNKNOWN_PLATFORM",
|
|
314
|
+
message: "Platform not supported. Please use Ts.ED platform (express, koa)"
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {ApolloServer, ApolloServerOptions, ApolloServerPlugin} from "@apollo/server";
|
|
2
|
+
import {ApolloServerPluginLandingPageDisabled} from "@apollo/server/plugin/disabled";
|
|
3
|
+
import {ApolloServerPluginDrainHttpServer} from "@apollo/server/plugin/drainHttpServer";
|
|
4
|
+
import {ApolloServerPluginLandingPageLocalDefault} from "@apollo/server/plugin/landingPage/default";
|
|
5
|
+
import type {IExecutableSchemaDefinition} from "@graphql-tools/schema";
|
|
6
|
+
import {Constant, context, Inject, InjectorService, LocalsContainer, Provider, Service} from "@tsed/di";
|
|
7
|
+
import {Logger} from "@tsed/logger";
|
|
8
|
+
import {PlatformApplication, PlatformContext} from "@tsed/platform-http";
|
|
9
|
+
import type {GraphQLSchema} from "graphql";
|
|
10
|
+
import Http from "http";
|
|
11
|
+
import Https from "https";
|
|
12
|
+
|
|
13
|
+
import {APOLLO_CONTEXT, DATASOURCES_PROVIDERS} from "../constants/constants.js";
|
|
14
|
+
import {ApolloContext} from "../interfaces/ApolloContext.js";
|
|
15
|
+
import type {ApolloCustomServerCB, ApolloSettings} from "../interfaces/ApolloSettings.js";
|
|
16
|
+
|
|
17
|
+
@Service()
|
|
18
|
+
export class ApolloService {
|
|
19
|
+
@Constant("PLATFORM_NAME")
|
|
20
|
+
platformName: string;
|
|
21
|
+
|
|
22
|
+
@Inject()
|
|
23
|
+
protected logger: Logger;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
*
|
|
27
|
+
* @type {Map<any, any>}
|
|
28
|
+
* @private
|
|
29
|
+
*/
|
|
30
|
+
protected servers: Map<
|
|
31
|
+
string,
|
|
32
|
+
{
|
|
33
|
+
instance: ApolloServer<ApolloContext>;
|
|
34
|
+
schema?: GraphQLSchema;
|
|
35
|
+
typeDefs?: IExecutableSchemaDefinition<ApolloContext>["typeDefs"];
|
|
36
|
+
resolvers?: IExecutableSchemaDefinition<ApolloContext>["resolvers"];
|
|
37
|
+
}
|
|
38
|
+
> = new Map();
|
|
39
|
+
|
|
40
|
+
@Inject()
|
|
41
|
+
private app: PlatformApplication<any>;
|
|
42
|
+
|
|
43
|
+
@Inject(Http.Server)
|
|
44
|
+
private httpServer: Http.Server | null;
|
|
45
|
+
|
|
46
|
+
@Inject(Https.Server)
|
|
47
|
+
private httpsServer: Https.Server | null;
|
|
48
|
+
|
|
49
|
+
@Inject()
|
|
50
|
+
private injector: InjectorService;
|
|
51
|
+
|
|
52
|
+
async createServer(id: string, settings: ApolloSettings) {
|
|
53
|
+
if (!this.has(id)) {
|
|
54
|
+
try {
|
|
55
|
+
const {dataSources, path, middlewareOptions = {}, server: customServer, ...config} = settings;
|
|
56
|
+
|
|
57
|
+
this.logger.info(`Create server with Apollo for: ${id}`);
|
|
58
|
+
this.logger.debug(`options: ${JSON.stringify({path})}`);
|
|
59
|
+
|
|
60
|
+
const plugins = await this.getPlugins(settings);
|
|
61
|
+
|
|
62
|
+
const server = this.createInstance(
|
|
63
|
+
{
|
|
64
|
+
...config,
|
|
65
|
+
plugins
|
|
66
|
+
} as never,
|
|
67
|
+
customServer
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (server) {
|
|
71
|
+
this.servers.set(id || "default", {
|
|
72
|
+
instance: server,
|
|
73
|
+
schema: settings.schema,
|
|
74
|
+
typeDefs: settings.typeDefs,
|
|
75
|
+
resolvers: settings.resolvers
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await server.start();
|
|
79
|
+
|
|
80
|
+
const contextHandler = this.createContextHandler(server, settings);
|
|
81
|
+
|
|
82
|
+
switch (this.platformName) {
|
|
83
|
+
case "express":
|
|
84
|
+
const {expressMiddleware} = await import("@apollo/server/express4");
|
|
85
|
+
|
|
86
|
+
this.app.use(
|
|
87
|
+
path,
|
|
88
|
+
expressMiddleware(server as any, {
|
|
89
|
+
...middlewareOptions,
|
|
90
|
+
context: contextHandler
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
case "koa":
|
|
96
|
+
const {koaMiddleware} = await import("@as-integrations/koa");
|
|
97
|
+
|
|
98
|
+
this.app.use(
|
|
99
|
+
path,
|
|
100
|
+
koaMiddleware(server as any, {
|
|
101
|
+
...middlewareOptions,
|
|
102
|
+
context: contextHandler
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
this.logger.warn({
|
|
108
|
+
event: "APOLLO_UNKNOWN_PLATFORM",
|
|
109
|
+
message: "Platform not supported. Please use Ts.ED platform (express, koa)"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch (er) {
|
|
114
|
+
this.logger.error({
|
|
115
|
+
event: "APOLLO_BOOTSTRAP_ERROR",
|
|
116
|
+
error_name: er.name,
|
|
117
|
+
message: er.message,
|
|
118
|
+
stack: er.stack
|
|
119
|
+
});
|
|
120
|
+
throw er;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return this.get(id)!;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get an instance of ApolloServer from his id
|
|
129
|
+
* @returns ApolloServer
|
|
130
|
+
*/
|
|
131
|
+
get(id: string = "default"): ApolloServer<ApolloContext> | undefined {
|
|
132
|
+
return this.servers.get(id)?.instance;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get schema of the ApolloServer from his id
|
|
137
|
+
* @returns GraphQLSchema
|
|
138
|
+
*/
|
|
139
|
+
getSchema(id: string = "default") {
|
|
140
|
+
return this.servers.get(id)?.schema;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get TypeDefs of the ApolloServer from his id
|
|
145
|
+
*/
|
|
146
|
+
getTypeDefs(id: string = "default") {
|
|
147
|
+
return this.servers.get(id)?.typeDefs;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getResolvers(id: string = "default") {
|
|
151
|
+
return this.servers.get(id)?.resolvers;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
*
|
|
156
|
+
* @param {string} id
|
|
157
|
+
* @returns {boolean}
|
|
158
|
+
*/
|
|
159
|
+
has(id: string = "default"): boolean {
|
|
160
|
+
return this.servers.has(id);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* create a new dataSources function to use with apollo server config
|
|
165
|
+
*/
|
|
166
|
+
createContextHandler(server: ApolloServer<ApolloContext>, settings: ApolloSettings) {
|
|
167
|
+
const {injector} = this;
|
|
168
|
+
const dataSourcesContainer = injector.getProviders(DATASOURCES_PROVIDERS).reduce((map, provider) => {
|
|
169
|
+
let {name} = provider.store.get(DATASOURCES_PROVIDERS);
|
|
170
|
+
|
|
171
|
+
name = name || provider.className;
|
|
172
|
+
|
|
173
|
+
const sourceName = `${name[0].toLowerCase()}${name.slice(1)}`;
|
|
174
|
+
map.set(sourceName, provider);
|
|
175
|
+
|
|
176
|
+
return map;
|
|
177
|
+
}, new Map<string, Provider>());
|
|
178
|
+
|
|
179
|
+
return async () => {
|
|
180
|
+
const $ctx = context<PlatformContext>();
|
|
181
|
+
const apolloContext: ApolloContext = {
|
|
182
|
+
dataSources: {
|
|
183
|
+
...(settings.dataSources?.() || {})
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const alteredContext = await this.injector.alterAsync("$alterApolloContext", apolloContext, $ctx);
|
|
188
|
+
|
|
189
|
+
$ctx!.set(APOLLO_CONTEXT, alteredContext);
|
|
190
|
+
|
|
191
|
+
const locals = new LocalsContainer();
|
|
192
|
+
locals.set(APOLLO_CONTEXT, alteredContext);
|
|
193
|
+
locals.set(ApolloServer, server);
|
|
194
|
+
|
|
195
|
+
dataSourcesContainer.forEach((provider, key) => {
|
|
196
|
+
alteredContext.dataSources[key] = injector.invoke(provider.token, {locals});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return alteredContext;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
protected createInstance(options: ApolloServerOptions<ApolloContext>, server?: ApolloCustomServerCB<ApolloContext>) {
|
|
204
|
+
return server ? server(options) : new ApolloServer(options);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private async getPlugins(serverSettings: ApolloSettings): Promise<ApolloServerPlugin[]> {
|
|
208
|
+
const playground = serverSettings.playground || (serverSettings.playground === undefined && process.env.NODE_ENV !== "production");
|
|
209
|
+
|
|
210
|
+
const result = await this.injector.alterAsync(
|
|
211
|
+
"$alterApolloServerPlugins",
|
|
212
|
+
[
|
|
213
|
+
this.httpServer &&
|
|
214
|
+
ApolloServerPluginDrainHttpServer({
|
|
215
|
+
httpServer: this.httpServer
|
|
216
|
+
}),
|
|
217
|
+
this.httpsServer &&
|
|
218
|
+
ApolloServerPluginDrainHttpServer({
|
|
219
|
+
httpServer: this.httpsServer
|
|
220
|
+
}),
|
|
221
|
+
...(serverSettings.plugins || [])
|
|
222
|
+
].filter(Boolean),
|
|
223
|
+
serverSettings
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return result
|
|
227
|
+
.concat([playground ? ApolloServerPluginLandingPageLocalDefault({embed: true}) : ApolloServerPluginLandingPageDisabled()])
|
|
228
|
+
.filter(Boolean);
|
|
229
|
+
}
|
|
230
|
+
}
|