@fluojs/graphql 1.0.4 → 2.0.0
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/README.ko.md +153 -17
- package/README.md +158 -17
- package/dist/dataloader/dataloader.d.ts.map +1 -1
- package/dist/dataloader/dataloader.js +2 -3
- package/dist/decorators.d.ts +53 -0
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +123 -1
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +13 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/instance-of-patch.d.ts +21 -0
- package/dist/instance-of-patch.d.ts.map +1 -0
- package/dist/instance-of-patch.js +126 -0
- package/dist/metadata.d.ts +21 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +68 -7
- package/dist/module.d.ts +14 -10
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +69 -14
- package/dist/node/graphql-websocket-transport.d.ts +53 -0
- package/dist/node/graphql-websocket-transport.d.ts.map +1 -0
- package/dist/node/graphql-websocket-transport.js +219 -0
- package/dist/pipeline/input-pipeline.js +1 -1
- package/dist/schema/object-field-resolvers.d.ts +18 -0
- package/dist/schema/object-field-resolvers.d.ts.map +1 -0
- package/dist/schema/object-field-resolvers.js +86 -0
- package/dist/schema/schema.d.ts +5 -1
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +75 -39
- package/dist/service.d.ts +10 -12
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +191 -303
- package/dist/transport/transport.d.ts.map +1 -1
- package/dist/transport/transport.js +1 -1
- package/dist/types.d.ts +40 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +19 -1
- package/package.json +11 -10
package/dist/module.js
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
|
+
import { isForwardRef, isOptionalToken } from '@fluojs/di';
|
|
1
2
|
import { defineModule } from '@fluojs/runtime';
|
|
3
|
+
import { RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
2
4
|
import { GRAPHQL_INTERNAL_MODULE_OPTIONS_TOKEN } from './internal-tokens.js';
|
|
3
5
|
import { GraphqlEndpointController, GraphqlLifecycleService } from './service.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
function createGraphqlProviders(optionsProvider) {
|
|
7
|
+
return [optionsProvider, GraphqlLifecycleService];
|
|
8
|
+
}
|
|
9
|
+
function assertGraphqlAsyncModuleOptions(options) {
|
|
10
|
+
if (options === null || typeof options !== 'object') {
|
|
11
|
+
throw new TypeError('GraphqlModule.forRootAsync requires an options object.');
|
|
12
|
+
}
|
|
13
|
+
const unsupportedKey = Object.keys(options).find(key => key !== 'inject' && key !== 'useFactory');
|
|
14
|
+
if (unsupportedKey !== undefined) {
|
|
15
|
+
throw new TypeError(`GraphqlModule.forRootAsync does not support "${unsupportedKey}"; use only inject and useFactory.`);
|
|
16
|
+
}
|
|
17
|
+
if (typeof options.useFactory !== 'function') {
|
|
18
|
+
throw new TypeError('GraphqlModule.forRootAsync requires a useFactory function.');
|
|
19
|
+
}
|
|
20
|
+
if (options.inject !== undefined && !Array.isArray(options.inject)) {
|
|
21
|
+
throw new TypeError('GraphqlModule.forRootAsync inject must be an array of application tokens.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isContainer(value) {
|
|
25
|
+
return value !== null && typeof value === 'object' && 'has' in value && 'resolve' in value;
|
|
26
|
+
}
|
|
27
|
+
async function resolveAsyncDependency(container, token) {
|
|
28
|
+
if (isForwardRef(token)) {
|
|
29
|
+
return await container.resolve(token.forwardRef());
|
|
30
|
+
}
|
|
31
|
+
if (isOptionalToken(token)) {
|
|
32
|
+
return container.has(token.token) ? await container.resolve(token.token) : undefined;
|
|
33
|
+
}
|
|
34
|
+
return await container.resolve(token);
|
|
16
35
|
}
|
|
17
36
|
|
|
18
37
|
/**
|
|
@@ -30,8 +49,44 @@ export class GraphqlModule {
|
|
|
30
49
|
class GraphqlRootModule extends GraphqlModule {}
|
|
31
50
|
return defineModule(GraphqlRootModule, {
|
|
32
51
|
controllers: [GraphqlEndpointController],
|
|
33
|
-
middleware: [],
|
|
34
|
-
providers: createGraphqlProviders(
|
|
52
|
+
middleware: [GraphqlLifecycleService],
|
|
53
|
+
providers: createGraphqlProviders({
|
|
54
|
+
provide: GRAPHQL_INTERNAL_MODULE_OPTIONS_TOKEN,
|
|
55
|
+
useValue: options
|
|
56
|
+
})
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Registers GraphQL from options resolved by explicitly injected application dependencies.
|
|
62
|
+
*
|
|
63
|
+
* The factory runs once in each application context. Only `inject` and `useFactory` are
|
|
64
|
+
* supported; NestJS-style `imports`, `useClass`, `useExisting`, and implicit discovery are rejected.
|
|
65
|
+
*
|
|
66
|
+
* @param options Injected dependency tokens and the factory that resolves GraphQL options.
|
|
67
|
+
* @returns A module definition that resolves GraphQL configuration before endpoint lifecycle wiring begins.
|
|
68
|
+
*
|
|
69
|
+
* @throws {TypeError} When the options shape requests unsupported registration behavior.
|
|
70
|
+
*/
|
|
71
|
+
static forRootAsync(options) {
|
|
72
|
+
assertGraphqlAsyncModuleOptions(options);
|
|
73
|
+
class GraphqlAsyncRootModule extends GraphqlModule {}
|
|
74
|
+
return defineModule(GraphqlAsyncRootModule, {
|
|
75
|
+
controllers: [GraphqlEndpointController],
|
|
76
|
+
middleware: [GraphqlLifecycleService],
|
|
77
|
+
providers: createGraphqlProviders({
|
|
78
|
+
inject: [RUNTIME_CONTAINER],
|
|
79
|
+
provide: GRAPHQL_INTERNAL_MODULE_OPTIONS_TOKEN,
|
|
80
|
+
scope: 'singleton',
|
|
81
|
+
useFactory: async (...dependencies) => {
|
|
82
|
+
const [runtimeContainer] = dependencies;
|
|
83
|
+
if (!isContainer(runtimeContainer)) {
|
|
84
|
+
throw new TypeError('GraphqlModule.forRootAsync could not resolve the application container.');
|
|
85
|
+
}
|
|
86
|
+
const injectedDependencies = await Promise.all((options.inject ?? []).map(async token => await resolveAsyncDependency(runtimeContainer, token)));
|
|
87
|
+
return Reflect.apply(options.useFactory, options, injectedDependencies);
|
|
88
|
+
}
|
|
89
|
+
})
|
|
35
90
|
});
|
|
36
91
|
}
|
|
37
92
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { FrameworkRequest, HttpApplicationAdapter } from '@fluojs/http';
|
|
2
|
+
import type { ExecutionArgs, execute as executeGraphql, GraphQLError as GraphQLErrorType, subscribe as subscribeGraphql } from 'graphql';
|
|
3
|
+
interface GraphqlNodeWebSocketLimits {
|
|
4
|
+
maxConnections: number;
|
|
5
|
+
maxOperationsPerConnection: number;
|
|
6
|
+
maxPayloadBytes: number;
|
|
7
|
+
}
|
|
8
|
+
interface GraphqlSubscribePayload {
|
|
9
|
+
operationName?: string | null;
|
|
10
|
+
query: string;
|
|
11
|
+
variables?: Record<string, unknown> | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Describes a GraphQL-over-WebSocket subscription forwarded to application hooks.
|
|
15
|
+
*/
|
|
16
|
+
export interface GraphqlNodeWebSocketSubscribeRequest {
|
|
17
|
+
connectionParams?: Record<string, unknown>;
|
|
18
|
+
operationId: string;
|
|
19
|
+
payload: GraphqlSubscribePayload;
|
|
20
|
+
request: FrameworkRequest;
|
|
21
|
+
socket: object;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Represents a registered Node GraphQL WebSocket transport.
|
|
25
|
+
*/
|
|
26
|
+
export interface GraphqlNodeWebSocketTransport {
|
|
27
|
+
/**
|
|
28
|
+
* Releases websocket listeners, clients, and GraphQL protocol resources.
|
|
29
|
+
*
|
|
30
|
+
* @returns A promise that resolves when cleanup completes or rejects with all current cleanup failures.
|
|
31
|
+
*/
|
|
32
|
+
dispose(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
interface GraphqlNodeWebSocketTransportOptions {
|
|
35
|
+
adapter: HttpApplicationAdapter;
|
|
36
|
+
connectionInitWaitTimeoutMs?: number;
|
|
37
|
+
execute: typeof executeGraphql;
|
|
38
|
+
keepAliveMs?: number;
|
|
39
|
+
limits?: GraphqlNodeWebSocketLimits;
|
|
40
|
+
onComplete: (socketKey: object, operationId: string) => Promise<void> | void;
|
|
41
|
+
onDisconnect: (socketKey: object) => Promise<void> | void;
|
|
42
|
+
onSubscribe: (request: GraphqlNodeWebSocketSubscribeRequest) => Promise<ExecutionArgs | readonly GraphQLErrorType[]>;
|
|
43
|
+
subscribe: typeof subscribeGraphql;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Registers GraphQL-over-WebSocket upgrade handling for a Node HTTP/S adapter.
|
|
47
|
+
*
|
|
48
|
+
* @param options Transport dependencies and WebSocket lifecycle hooks.
|
|
49
|
+
* @returns A transport that unregisters upgrade handling and disposes WebSocket clients.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createNodeGraphqlWebSocketTransport(options: GraphqlNodeWebSocketTransportOptions): Promise<GraphqlNodeWebSocketTransport>;
|
|
52
|
+
export {};
|
|
53
|
+
//# sourceMappingURL=graphql-websocket-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql-websocket-transport.d.ts","sourceRoot":"","sources":["../../src/node/graphql-websocket-transport.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC7E,OAAO,KAAK,EACV,aAAa,EACb,OAAO,IAAI,cAAc,EACzB,YAAY,IAAI,gBAAgB,EAChC,SAAS,IAAI,gBAAgB,EAC9B,MAAM,SAAS,CAAC;AAgBjB,UAAU,0BAA0B;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,0BAA0B,EAAE,MAAM,CAAC;IACnC,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,uBAAuB;IAC/B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,WAAW,oCAAoC;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,uBAAuB,CAAC;IACjC,OAAO,EAAE,gBAAgB,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,UAAU,oCAAoC;IAC5C,OAAO,EAAE,sBAAsB,CAAC;IAChC,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,OAAO,EAAE,OAAO,cAAc,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,0BAA0B,CAAC;IACpC,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC7E,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1D,WAAW,EAAE,CAAC,OAAO,EAAE,oCAAoC,KAAK,OAAO,CAAC,aAAa,GAAG,SAAS,gBAAgB,EAAE,CAAC,CAAC;IACrH,SAAS,EAAE,OAAO,gBAAgB,CAAC;CACpC;AA2ID;;;;;GAKG;AACH,wBAAsB,mCAAmC,CACvD,OAAO,EAAE,oCAAoC,GAC5C,OAAO,CAAC,6BAA6B,CAAC,CAkIxC"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { handleProtocols } from 'graphql-ws';
|
|
2
|
+
import { useServer } from 'graphql-ws/lib/use/ws';
|
|
3
|
+
import { WebSocketServer } from 'ws';
|
|
4
|
+
import { isGraphqlPath } from '../transport/transport.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Describes a GraphQL-over-WebSocket subscription forwarded to application hooks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents a registered Node GraphQL WebSocket transport.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function hasNodeUpgradeServer(value) {
|
|
15
|
+
if (typeof value !== 'object' || value === null) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const server = value;
|
|
19
|
+
return typeof server.on === 'function' && typeof server.off === 'function';
|
|
20
|
+
}
|
|
21
|
+
function isConnectionParamsRecord(value) {
|
|
22
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
function buildFrameworkRequestFromIncomingMessage(request) {
|
|
25
|
+
const requestUrl = new URL(request.url ?? '/graphql', 'http://localhost');
|
|
26
|
+
return {
|
|
27
|
+
cookies: {},
|
|
28
|
+
headers: request.headers,
|
|
29
|
+
method: request.method ?? 'GET',
|
|
30
|
+
params: {},
|
|
31
|
+
path: requestUrl.pathname,
|
|
32
|
+
query: Object.fromEntries(requestUrl.searchParams.entries()),
|
|
33
|
+
raw: request,
|
|
34
|
+
url: requestUrl.pathname + requestUrl.search
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function closeWebSocketServer(server) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
server.close(error => {
|
|
40
|
+
if (error?.message === 'The server is not running') {
|
|
41
|
+
resolve();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (error) {
|
|
45
|
+
reject(error);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
resolve();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function collectCleanupError(errors, error) {
|
|
53
|
+
if (error instanceof AggregateError) {
|
|
54
|
+
for (const nested of error.errors) {
|
|
55
|
+
collectCleanupError(errors, nested);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (errors.includes(error)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
errors.push(error);
|
|
63
|
+
}
|
|
64
|
+
function resolveUpgradeServer(adapter) {
|
|
65
|
+
if (typeof adapter.getServer !== 'function') {
|
|
66
|
+
throw new Error('GraphQL websocket subscriptions require an HTTP adapter with getServer(). Use the Node HTTP adapter or provide a compatible adapter implementation.');
|
|
67
|
+
}
|
|
68
|
+
const server = adapter.getServer();
|
|
69
|
+
if (!hasNodeUpgradeServer(server)) {
|
|
70
|
+
throw new Error('GraphQL websocket subscriptions require adapter.getServer() to return a Node HTTP/S server that supports upgrade listeners.');
|
|
71
|
+
}
|
|
72
|
+
return server;
|
|
73
|
+
}
|
|
74
|
+
function rejectWebSocketUpgrade(socket, statusCode, message) {
|
|
75
|
+
if (!socket.writable) {
|
|
76
|
+
socket.destroy();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const body = `${message}\n`;
|
|
80
|
+
socket.write([`HTTP/1.1 ${String(statusCode)} ${statusCode === 503 ? 'Service Unavailable' : 'Bad Request'}`, 'Connection: close', 'Content-Type: text/plain; charset=utf-8', `Content-Length: ${String(new TextEncoder().encode(body).byteLength)}`, '', body].join('\r\n'));
|
|
81
|
+
socket.destroy();
|
|
82
|
+
}
|
|
83
|
+
function createUpgradeListener(websocketServer, limits) {
|
|
84
|
+
return (request, socket, head) => {
|
|
85
|
+
const targetPath = new URL(request.url ?? '/', 'http://localhost').pathname;
|
|
86
|
+
if (!isGraphqlPath(targetPath)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (limits && websocketServer.clients.size >= limits.maxConnections) {
|
|
90
|
+
rejectWebSocketUpgrade(socket, 503, 'GraphQL websocket connection count exceeds the configured limit.');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
websocketServer.handleUpgrade(request, socket, head, websocket => {
|
|
94
|
+
websocketServer.emit('connection', websocket, request);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function createSubscribeRequest(context, message) {
|
|
99
|
+
return {
|
|
100
|
+
connectionParams: isConnectionParamsRecord(context.connectionParams) ? context.connectionParams : undefined,
|
|
101
|
+
operationId: message.id,
|
|
102
|
+
payload: message.payload,
|
|
103
|
+
request: buildFrameworkRequestFromIncomingMessage(context.extra.request),
|
|
104
|
+
socket: context.extra.socket
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Registers GraphQL-over-WebSocket upgrade handling for a Node HTTP/S adapter.
|
|
110
|
+
*
|
|
111
|
+
* @param options Transport dependencies and WebSocket lifecycle hooks.
|
|
112
|
+
* @returns A transport that unregisters upgrade handling and disposes WebSocket clients.
|
|
113
|
+
*/
|
|
114
|
+
export async function createNodeGraphqlWebSocketTransport(options) {
|
|
115
|
+
const upgradeServer = resolveUpgradeServer(options.adapter);
|
|
116
|
+
const disconnectErrors = [];
|
|
117
|
+
const disconnectedSockets = new Set();
|
|
118
|
+
const pendingDisconnects = new Set();
|
|
119
|
+
let websocketDisposableDisposed = false;
|
|
120
|
+
let websocketServerClosed = false;
|
|
121
|
+
let inFlightDispose;
|
|
122
|
+
const websocketServer = new WebSocketServer({
|
|
123
|
+
handleProtocols: protocols => handleProtocols(protocols),
|
|
124
|
+
maxPayload: options.limits?.maxPayloadBytes ?? 0,
|
|
125
|
+
noServer: true
|
|
126
|
+
});
|
|
127
|
+
const upgradeListener = createUpgradeListener(websocketServer, options.limits);
|
|
128
|
+
const drainDisconnectErrors = cleanupErrors => {
|
|
129
|
+
for (const error of disconnectErrors.splice(0, disconnectErrors.length)) {
|
|
130
|
+
collectCleanupError(cleanupErrors, error);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const trackDisconnect = socketKey => {
|
|
134
|
+
if (disconnectedSockets.has(socketKey)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
disconnectedSockets.add(socketKey);
|
|
138
|
+
const pendingDisconnect = Promise.resolve(options.onDisconnect(socketKey)).catch(error => {
|
|
139
|
+
disconnectErrors.push(error);
|
|
140
|
+
}).finally(() => {
|
|
141
|
+
pendingDisconnects.delete(pendingDisconnect);
|
|
142
|
+
});
|
|
143
|
+
pendingDisconnects.add(pendingDisconnect);
|
|
144
|
+
};
|
|
145
|
+
const trackConnection = websocket => {
|
|
146
|
+
websocket.once('close', () => trackDisconnect(websocket));
|
|
147
|
+
};
|
|
148
|
+
const websocketDisposable = useServer({
|
|
149
|
+
connectionInitWaitTimeout: options.connectionInitWaitTimeoutMs,
|
|
150
|
+
execute: options.execute,
|
|
151
|
+
onComplete: async (context, message) => {
|
|
152
|
+
await options.onComplete(context.extra.socket, message.id);
|
|
153
|
+
},
|
|
154
|
+
onDisconnect: context => {
|
|
155
|
+
trackDisconnect(context.extra.socket);
|
|
156
|
+
},
|
|
157
|
+
onSubscribe: async (context, message) => options.onSubscribe(createSubscribeRequest(context, message)),
|
|
158
|
+
subscribe: options.subscribe
|
|
159
|
+
}, websocketServer, options.keepAliveMs);
|
|
160
|
+
websocketServer.on('connection', trackConnection);
|
|
161
|
+
upgradeServer.on('upgrade', upgradeListener);
|
|
162
|
+
const runDispose = async () => {
|
|
163
|
+
const cleanupErrors = [];
|
|
164
|
+
drainDisconnectErrors(cleanupErrors);
|
|
165
|
+
websocketServer.off('connection', trackConnection);
|
|
166
|
+
upgradeServer.off('upgrade', upgradeListener);
|
|
167
|
+
for (const client of websocketServer.clients) {
|
|
168
|
+
client.terminate();
|
|
169
|
+
}
|
|
170
|
+
while (pendingDisconnects.size > 0) {
|
|
171
|
+
const disconnectResults = await Promise.allSettled([...pendingDisconnects]);
|
|
172
|
+
for (const result of disconnectResults) {
|
|
173
|
+
if (result.status === 'rejected') {
|
|
174
|
+
collectCleanupError(cleanupErrors, result.reason);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
drainDisconnectErrors(cleanupErrors);
|
|
179
|
+
if (!websocketDisposableDisposed) {
|
|
180
|
+
try {
|
|
181
|
+
await websocketDisposable.dispose();
|
|
182
|
+
websocketDisposableDisposed = true;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
collectCleanupError(cleanupErrors, error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!websocketServerClosed) {
|
|
188
|
+
try {
|
|
189
|
+
await closeWebSocketServer(websocketServer);
|
|
190
|
+
websocketServerClosed = true;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
collectCleanupError(cleanupErrors, error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
while (pendingDisconnects.size > 0) {
|
|
196
|
+
const lateDisconnectResults = await Promise.allSettled([...pendingDisconnects]);
|
|
197
|
+
for (const result of lateDisconnectResults) {
|
|
198
|
+
if (result.status === 'rejected') {
|
|
199
|
+
collectCleanupError(cleanupErrors, result.reason);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
drainDisconnectErrors(cleanupErrors);
|
|
204
|
+
if (cleanupErrors.length === 1) {
|
|
205
|
+
throw cleanupErrors[0];
|
|
206
|
+
}
|
|
207
|
+
if (cleanupErrors.length > 1) {
|
|
208
|
+
throw new AggregateError(cleanupErrors, 'Failed to dispose GraphQL websocket transport resources.');
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
async dispose() {
|
|
213
|
+
inFlightDispose ??= runDispose().finally(() => {
|
|
214
|
+
inFlightDispose = undefined;
|
|
215
|
+
});
|
|
216
|
+
await inFlightDispose;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getDtoValidationSchema } from '@fluojs/core/
|
|
1
|
+
import { getDtoValidationSchema } from '@fluojs/core/request-pipeline';
|
|
2
2
|
import { DefaultValidator } from '@fluojs/validation';
|
|
3
3
|
import { isGraphqlListTypeRef } from '../types.js';
|
|
4
4
|
const defaultValidator = new DefaultValidator();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GraphQLFieldConfigArgumentMap, GraphQLFieldConfigMap, GraphQLOutputType } from 'graphql';
|
|
2
|
+
import type { GraphQLContext as FluoGraphQLContext, GraphqlRootOutputType, ResolverDescriptor, ResolverHandlerDescriptor } from '../types.js';
|
|
3
|
+
type ResolveOutputType = (outputType: GraphqlRootOutputType) => GraphQLOutputType;
|
|
4
|
+
type ResolveInputArgs = (handler: ResolverHandlerDescriptor) => GraphQLFieldConfigArgumentMap;
|
|
5
|
+
type InvokeObjectFieldResolver = (descriptor: ResolverDescriptor, handler: ResolverHandlerDescriptor, args: Record<string, unknown>, source: unknown, contextValue: FluoGraphQLContext) => Promise<unknown>;
|
|
6
|
+
/**
|
|
7
|
+
* Indexes object field resolver descriptors and attaches them to matching code-first object types.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ObjectFieldResolverRegistry {
|
|
10
|
+
private readonly attachedTypeNames;
|
|
11
|
+
private readonly entriesByTypeName;
|
|
12
|
+
constructor(descriptors: readonly ResolverDescriptor[]);
|
|
13
|
+
attach(typeName: string, fields: GraphQLFieldConfigMap<unknown, FluoGraphQLContext>, resolveOutputType: ResolveOutputType, resolveInputArgs: ResolveInputArgs, invokeResolver: InvokeObjectFieldResolver): GraphQLFieldConfigMap<unknown, FluoGraphQLContext>;
|
|
14
|
+
createMethodArguments(handler: ResolverHandlerDescriptor, input: unknown, parent: unknown, contextValue: FluoGraphQLContext): unknown[];
|
|
15
|
+
assertAllTargetsAttached(): void;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=object-field-resolvers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object-field-resolvers.d.ts","sourceRoot":"","sources":["../../src/schema/object-field-resolvers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,6BAA6B,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEvG,OAAO,KAAK,EACV,cAAc,IAAI,kBAAkB,EACpC,qBAAqB,EACrB,kBAAkB,EAClB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAOrB,KAAK,iBAAiB,GAAG,CAAC,UAAU,EAAE,qBAAqB,KAAK,iBAAiB,CAAC;AAClF,KAAK,gBAAgB,GAAG,CAAC,OAAO,EAAE,yBAAyB,KAAK,6BAA6B,CAAC;AAE9F,KAAK,yBAAyB,GAAG,CAC/B,UAAU,EAAE,kBAAkB,EAC9B,OAAO,EAAE,yBAAyB,EAClC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,OAAO,EACf,YAAY,EAAE,kBAAkB,KAC7B,OAAO,CAAC,OAAO,CAAC,CAAC;AAMtB;;GAEG;AACH,qBAAa,2BAA2B;IACtC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAqB;IACvD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA4D;gBAElF,WAAW,EAAE,SAAS,kBAAkB,EAAE;IAqBtD,MAAM,CACJ,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,qBAAqB,CAAC,OAAO,EAAE,kBAAkB,CAAC,EAC1D,iBAAiB,EAAE,iBAAiB,EACpC,gBAAgB,EAAE,gBAAgB,EAClC,cAAc,EAAE,yBAAyB,GACxC,qBAAqB,CAAC,OAAO,EAAE,kBAAkB,CAAC;IAmCrD,qBAAqB,CACnB,OAAO,EAAE,yBAAyB,EAClC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,OAAO,EACf,YAAY,EAAE,kBAAkB,GAC/B,OAAO,EAAE;IAuBZ,wBAAwB,IAAI,IAAI;CASjC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { GraphQLNonNull } from 'graphql';
|
|
2
|
+
function assertNever(value) {
|
|
3
|
+
throw new Error(`Unsupported GraphQL field resolver parameter binding: ${String(value)}`);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Indexes object field resolver descriptors and attaches them to matching code-first object types.
|
|
8
|
+
*/
|
|
9
|
+
export class ObjectFieldResolverRegistry {
|
|
10
|
+
attachedTypeNames = new Set();
|
|
11
|
+
entriesByTypeName = new Map();
|
|
12
|
+
constructor(descriptors) {
|
|
13
|
+
for (const descriptor of descriptors) {
|
|
14
|
+
for (const handler of descriptor.handlers) {
|
|
15
|
+
if (handler.type !== 'field') {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const fields = this.entriesByTypeName.get(descriptor.typeName) ?? new Map();
|
|
19
|
+
if (fields.has(handler.fieldName)) {
|
|
20
|
+
throw new Error(`GraphQL schema conflict: object field resolver "${descriptor.typeName}.${handler.fieldName}" is registered more than once.`);
|
|
21
|
+
}
|
|
22
|
+
fields.set(handler.fieldName, {
|
|
23
|
+
descriptor,
|
|
24
|
+
handler
|
|
25
|
+
});
|
|
26
|
+
this.entriesByTypeName.set(descriptor.typeName, fields);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
attach(typeName, fields, resolveOutputType, resolveInputArgs, invokeResolver) {
|
|
31
|
+
const resolverFields = this.entriesByTypeName.get(typeName);
|
|
32
|
+
if (!resolverFields) {
|
|
33
|
+
return fields;
|
|
34
|
+
}
|
|
35
|
+
this.attachedTypeNames.add(typeName);
|
|
36
|
+
const attachedFields = {
|
|
37
|
+
...fields
|
|
38
|
+
};
|
|
39
|
+
for (const [fieldName, entry] of resolverFields) {
|
|
40
|
+
const existingField = attachedFields[fieldName];
|
|
41
|
+
const outputType = entry.handler.outputType ? resolveOutputType(entry.handler.outputType) : existingField?.type;
|
|
42
|
+
if (!outputType) {
|
|
43
|
+
throw new Error(`GraphQL object field resolver "${typeName}.${fieldName}" must target an existing field or declare a type.`);
|
|
44
|
+
}
|
|
45
|
+
const hasExplicitNonNullableNewField = existingField === undefined && entry.handler.outputType !== undefined && entry.handler.nullable === false;
|
|
46
|
+
attachedFields[fieldName] = {
|
|
47
|
+
...existingField,
|
|
48
|
+
...(entry.handler.inputClass ? {
|
|
49
|
+
args: resolveInputArgs(entry.handler)
|
|
50
|
+
} : {}),
|
|
51
|
+
resolve: async (source, args, contextValue) => invokeResolver(entry.descriptor, entry.handler, args, source, contextValue),
|
|
52
|
+
type: hasExplicitNonNullableNewField ? new GraphQLNonNull(outputType) : outputType
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return attachedFields;
|
|
56
|
+
}
|
|
57
|
+
createMethodArguments(handler, input, parent, contextValue) {
|
|
58
|
+
const lastBinding = handler.parameterBindings.at(-1);
|
|
59
|
+
const methodArguments = Array.from({
|
|
60
|
+
length: (lastBinding?.index ?? -1) + 1
|
|
61
|
+
});
|
|
62
|
+
for (const binding of handler.parameterBindings) {
|
|
63
|
+
switch (binding.kind) {
|
|
64
|
+
case 'input':
|
|
65
|
+
methodArguments[binding.index] = input;
|
|
66
|
+
break;
|
|
67
|
+
case 'parent':
|
|
68
|
+
methodArguments[binding.index] = parent;
|
|
69
|
+
break;
|
|
70
|
+
case 'context':
|
|
71
|
+
methodArguments[binding.index] = contextValue;
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
assertNever(binding.kind);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return methodArguments;
|
|
78
|
+
}
|
|
79
|
+
assertAllTargetsAttached() {
|
|
80
|
+
for (const typeName of this.entriesByTypeName.keys()) {
|
|
81
|
+
if (!this.attachedTypeNames.has(typeName)) {
|
|
82
|
+
throw new Error(`GraphQL object field resolver target type "${typeName}" is not reachable from a code-first root operation output type.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Container } from '@fluojs/di';
|
|
2
|
-
import type { GraphQLError as GraphQLErrorType, GraphQLList as GraphQLListType, GraphQLObjectType as GraphQLObjectTypeType, GraphQLSchema as GraphQLSchemaType,
|
|
2
|
+
import type { GraphQLError as GraphQLErrorType, GraphQLList as GraphQLListType, GraphQLNonNull as GraphQLNonNullType, GraphQLObjectType as GraphQLObjectTypeType, GraphQLScalarType, GraphQLSchema as GraphQLSchemaType, GraphQLUnionType as GraphQLUnionTypeType } from 'graphql';
|
|
3
3
|
import { type ResolverDescriptor } from '../types.js';
|
|
4
4
|
type YogaGraphqlDeps = {
|
|
5
5
|
GraphQLError: typeof GraphQLErrorType;
|
|
@@ -8,11 +8,15 @@ type YogaGraphqlDeps = {
|
|
|
8
8
|
GraphQLID: GraphQLScalarType;
|
|
9
9
|
GraphQLInt: GraphQLScalarType;
|
|
10
10
|
GraphQLList: typeof GraphQLListType;
|
|
11
|
+
GraphQLNonNull: typeof GraphQLNonNullType;
|
|
11
12
|
GraphQLObjectType: typeof GraphQLObjectTypeType;
|
|
12
13
|
GraphQLSchema: typeof GraphQLSchemaType;
|
|
13
14
|
GraphQLString: GraphQLScalarType;
|
|
14
15
|
GraphQLUnionType: typeof GraphQLUnionTypeType;
|
|
15
16
|
buildSchema: (source: string) => GraphQLSchemaType;
|
|
17
|
+
createGraphQLError: (message: string, options: {
|
|
18
|
+
extensions?: Record<string, unknown>;
|
|
19
|
+
}) => GraphQLErrorType;
|
|
16
20
|
};
|
|
17
21
|
/**
|
|
18
22
|
* Resolve schema.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/schema/schema.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/schema/schema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,OAAO,KAAK,EAEV,YAAY,IAAI,gBAAgB,EAMhC,WAAW,IAAI,eAAe,EAC9B,cAAc,IAAI,kBAAkB,EACpC,iBAAiB,IAAI,qBAAqB,EAE1C,iBAAiB,EACjB,aAAa,IAAI,iBAAiB,EAClC,gBAAgB,IAAI,oBAAoB,EACzC,MAAM,SAAS,CAAC;AAGjB,OAAO,EAOL,KAAK,kBAAkB,EAGxB,MAAM,aAAa,CAAC;AAGrB,KAAK,eAAe,GAAG;IACrB,YAAY,EAAE,OAAO,gBAAgB,CAAC;IACtC,cAAc,EAAE,iBAAiB,CAAC;IAClC,YAAY,EAAE,iBAAiB,CAAC;IAChC,SAAS,EAAE,iBAAiB,CAAC;IAC7B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,WAAW,EAAE,OAAO,eAAe,CAAC;IACpC,cAAc,EAAE,OAAO,kBAAkB,CAAC;IAC1C,iBAAiB,EAAE,OAAO,qBAAqB,CAAC;IAChD,aAAa,EAAE,OAAO,iBAAiB,CAAC;IACxC,aAAa,EAAE,iBAAiB,CAAC;IACjC,gBAAgB,EAAE,OAAO,oBAAoB,CAAC;IAC9C,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,iBAAiB,CAAC;IACnD,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,KAAK,gBAAgB,CAAC;CAC9G,CAAC;AA4fF;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,eAAe,EACrB,aAAa,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,EACrD,qBAAqB,EAAE,MAAM,iBAAiB,EAC9C,mCAAmC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAC5D,iBAAiB,CAWnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,eAAe,EACrB,gBAAgB,EAAE,SAAS,EAC3B,mBAAmB,EAAE,kBAAkB,EAAE,EACzC,mCAAmC,GAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAe,GACvE,iBAAiB,CA6EnB"}
|