@tsed/apollo 8.0.1 → 8.0.3

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 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.1",
5
+ "version": "8.0.3",
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.1",
32
- "@tsed/core": "8.0.1",
33
- "@tsed/di": "8.0.1",
32
+ "@tsed/barrels": "8.0.3",
33
+ "@tsed/core": "8.0.3",
34
+ "@tsed/di": "8.0.3",
34
35
  "@tsed/logger": "^6.7.8",
35
- "@tsed/platform-http": "8.0.1",
36
- "@tsed/typescript": "8.0.1",
36
+ "@tsed/platform-http": "8.0.3",
37
+ "@tsed/typescript": "8.0.3",
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.1",
47
- "@tsed/di": "8.0.1",
47
+ "@tsed/core": "8.0.3",
48
+ "@tsed/di": "8.0.3",
48
49
  "@tsed/logger": ">=6.7.5",
49
- "@tsed/platform-http": "8.0.1",
50
+ "@tsed/platform-http": "8.0.3",
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,2 @@
1
+ export const DATASOURCES_PROVIDERS = "graphql:datasources";
2
+ export const APOLLO_CONTEXT = Symbol.for("APOLLO_CONTEXT");
@@ -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,5 @@
1
+ import type {BaseContext} from "@apollo/server";
2
+
3
+ export interface ApolloContext extends BaseContext {
4
+ dataSources: Record<string, unknown>;
5
+ }
@@ -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
+ }