@constructive-io/graphql-server 2.16.3 → 2.17.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/esm/server.js +77 -10
- package/package.json +19 -19
- package/server.d.ts +14 -1
- package/server.js +75 -8
package/esm/server.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { getEnvOptions, getNodeEnv } from '@constructive-io/graphql-env';
|
|
2
2
|
import { Logger } from '@pgpmjs/logger';
|
|
3
|
-
import { healthz, poweredBy, trustProxy } from '@pgpmjs/server-utils';
|
|
3
|
+
import { healthz, poweredBy, svcCache, trustProxy } from '@pgpmjs/server-utils';
|
|
4
4
|
import { middleware as parseDomains } from '@constructive-io/url-domains';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import express from 'express';
|
|
7
7
|
// @ts-ignore
|
|
8
8
|
import graphqlUpload from 'graphql-upload';
|
|
9
|
-
import {
|
|
9
|
+
import { graphileCache } from 'graphile-cache';
|
|
10
|
+
import { getPgPool, pgCache } from 'pg-cache';
|
|
10
11
|
import requestIp from 'request-ip';
|
|
11
12
|
import { createApiMiddleware } from './middleware/api';
|
|
12
13
|
import { createAuthenticateMiddleware } from './middleware/auth';
|
|
@@ -24,11 +25,17 @@ export const GraphQLServer = (rawOpts = {}) => {
|
|
|
24
25
|
class Server {
|
|
25
26
|
app;
|
|
26
27
|
opts;
|
|
28
|
+
listenClient = null;
|
|
29
|
+
listenRelease = null;
|
|
30
|
+
shuttingDown = false;
|
|
31
|
+
closed = false;
|
|
32
|
+
httpServer = null;
|
|
27
33
|
constructor(opts) {
|
|
28
34
|
this.opts = getEnvOptions(opts);
|
|
35
|
+
const effectiveOpts = this.opts;
|
|
29
36
|
const app = express();
|
|
30
|
-
const api = createApiMiddleware(
|
|
31
|
-
const authenticate = createAuthenticateMiddleware(
|
|
37
|
+
const api = createApiMiddleware(effectiveOpts);
|
|
38
|
+
const authenticate = createAuthenticateMiddleware(effectiveOpts);
|
|
32
39
|
const requestLogger = (req, res, next) => {
|
|
33
40
|
const headerRequestId = req.header('x-request-id');
|
|
34
41
|
const reqId = headerRequestId || randomUUID();
|
|
@@ -50,13 +57,13 @@ class Server {
|
|
|
50
57
|
};
|
|
51
58
|
// Log startup config in dev mode
|
|
52
59
|
if (isDev()) {
|
|
53
|
-
log.debug(`Database: ${
|
|
54
|
-
log.debug(`Meta schemas: ${
|
|
60
|
+
log.debug(`Database: ${effectiveOpts.pg?.database}@${effectiveOpts.pg?.host}:${effectiveOpts.pg?.port}`);
|
|
61
|
+
log.debug(`Meta schemas: ${effectiveOpts.api?.metaSchemas?.join(', ') || 'default'}`);
|
|
55
62
|
}
|
|
56
63
|
healthz(app);
|
|
57
|
-
trustProxy(app,
|
|
64
|
+
trustProxy(app, effectiveOpts.server.trustProxy);
|
|
58
65
|
// Warn if a global CORS override is set in production
|
|
59
|
-
const fallbackOrigin =
|
|
66
|
+
const fallbackOrigin = effectiveOpts.server?.origin?.trim();
|
|
60
67
|
if (fallbackOrigin && process.env.NODE_ENV === 'production') {
|
|
61
68
|
if (fallbackOrigin === '*') {
|
|
62
69
|
log.warn('CORS wildcard ("*") is enabled in production; this effectively disables CORS and is not recommended. Prefer per-API CORS via meta schema.');
|
|
@@ -73,7 +80,7 @@ class Server {
|
|
|
73
80
|
app.use(requestLogger);
|
|
74
81
|
app.use(api);
|
|
75
82
|
app.use(authenticate);
|
|
76
|
-
app.use(graphile(
|
|
83
|
+
app.use(graphile(effectiveOpts));
|
|
77
84
|
app.use(flush);
|
|
78
85
|
this.app = app;
|
|
79
86
|
}
|
|
@@ -89,6 +96,8 @@ class Server {
|
|
|
89
96
|
}
|
|
90
97
|
throw err;
|
|
91
98
|
});
|
|
99
|
+
this.httpServer = httpServer;
|
|
100
|
+
return httpServer;
|
|
92
101
|
}
|
|
93
102
|
async flush(databaseId) {
|
|
94
103
|
await flushService(this.opts, databaseId);
|
|
@@ -97,15 +106,25 @@ class Server {
|
|
|
97
106
|
return getPgPool(this.opts.pg);
|
|
98
107
|
}
|
|
99
108
|
addEventListener() {
|
|
109
|
+
if (this.shuttingDown)
|
|
110
|
+
return;
|
|
100
111
|
const pgPool = this.getPool();
|
|
101
112
|
pgPool.connect(this.listenForChanges.bind(this));
|
|
102
113
|
}
|
|
103
114
|
listenForChanges(err, client, release) {
|
|
104
115
|
if (err) {
|
|
105
116
|
this.error('Error connecting with notify listener', err);
|
|
106
|
-
|
|
117
|
+
if (!this.shuttingDown) {
|
|
118
|
+
setTimeout(() => this.addEventListener(), 5000);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (this.shuttingDown) {
|
|
123
|
+
release();
|
|
107
124
|
return;
|
|
108
125
|
}
|
|
126
|
+
this.listenClient = client;
|
|
127
|
+
this.listenRelease = release;
|
|
109
128
|
client.on('notification', ({ channel, payload }) => {
|
|
110
129
|
if (channel === 'schema:update' && payload) {
|
|
111
130
|
log.info('schema:update', payload);
|
|
@@ -114,12 +133,60 @@ class Server {
|
|
|
114
133
|
});
|
|
115
134
|
client.query('LISTEN "schema:update"');
|
|
116
135
|
client.on('error', (e) => {
|
|
136
|
+
if (this.shuttingDown) {
|
|
137
|
+
release();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
117
140
|
this.error('Error with database notify listener', e);
|
|
118
141
|
release();
|
|
119
142
|
this.addEventListener();
|
|
120
143
|
});
|
|
121
144
|
this.log('connected and listening for changes...');
|
|
122
145
|
}
|
|
146
|
+
async removeEventListener() {
|
|
147
|
+
if (!this.listenClient || !this.listenRelease) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const client = this.listenClient;
|
|
151
|
+
const release = this.listenRelease;
|
|
152
|
+
this.listenClient = null;
|
|
153
|
+
this.listenRelease = null;
|
|
154
|
+
client.removeAllListeners('notification');
|
|
155
|
+
client.removeAllListeners('error');
|
|
156
|
+
try {
|
|
157
|
+
await client.query('UNLISTEN "schema:update"');
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Ignore listener cleanup errors during shutdown.
|
|
161
|
+
}
|
|
162
|
+
release();
|
|
163
|
+
}
|
|
164
|
+
async close(opts = {}) {
|
|
165
|
+
const { closeCaches = false } = opts;
|
|
166
|
+
if (this.closed) {
|
|
167
|
+
if (closeCaches) {
|
|
168
|
+
await Server.closeCaches({ closePools: true });
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.closed = true;
|
|
173
|
+
this.shuttingDown = true;
|
|
174
|
+
await this.removeEventListener();
|
|
175
|
+
if (this.httpServer?.listening) {
|
|
176
|
+
await new Promise((resolve) => this.httpServer.close(() => resolve()));
|
|
177
|
+
}
|
|
178
|
+
if (closeCaches) {
|
|
179
|
+
await Server.closeCaches({ closePools: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
static async closeCaches(opts = {}) {
|
|
183
|
+
const { closePools = false } = opts;
|
|
184
|
+
svcCache.clear();
|
|
185
|
+
graphileCache.clear();
|
|
186
|
+
if (closePools) {
|
|
187
|
+
await pgCache.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
123
190
|
log(text) {
|
|
124
191
|
log.info(text);
|
|
125
192
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Constructive GraphQL Server",
|
|
6
6
|
"main": "index.js",
|
|
@@ -41,51 +41,51 @@
|
|
|
41
41
|
"backend"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@constructive-io/graphql-env": "^2.8.
|
|
45
|
-
"@constructive-io/graphql-types": "^2.12.
|
|
44
|
+
"@constructive-io/graphql-env": "^2.8.20",
|
|
45
|
+
"@constructive-io/graphql-types": "^2.12.14",
|
|
46
46
|
"@constructive-io/s3-utils": "^2.4.1",
|
|
47
47
|
"@constructive-io/upload-names": "^2.3.6",
|
|
48
48
|
"@constructive-io/url-domains": "^2.3.7",
|
|
49
49
|
"@graphile-contrib/pg-many-to-many": "^1.0.2",
|
|
50
|
-
"@pgpmjs/logger": "^1.3.
|
|
51
|
-
"@pgpmjs/server-utils": "^2.8.
|
|
52
|
-
"@pgpmjs/types": "^2.14.
|
|
50
|
+
"@pgpmjs/logger": "^1.3.8",
|
|
51
|
+
"@pgpmjs/server-utils": "^2.8.16",
|
|
52
|
+
"@pgpmjs/types": "^2.14.2",
|
|
53
53
|
"cors": "^2.8.5",
|
|
54
54
|
"express": "^5.2.1",
|
|
55
55
|
"graphile-build": "^4.14.1",
|
|
56
|
-
"graphile-cache": "^1.6.
|
|
57
|
-
"graphile-i18n": "^0.4.
|
|
58
|
-
"graphile-meta-schema": "^0.5.
|
|
59
|
-
"graphile-plugin-connection-filter": "^2.6.
|
|
60
|
-
"graphile-plugin-connection-filter-postgis": "^1.3.
|
|
61
|
-
"graphile-plugin-fulltext-filter": "^2.3.
|
|
56
|
+
"graphile-cache": "^1.6.16",
|
|
57
|
+
"graphile-i18n": "^0.4.17",
|
|
58
|
+
"graphile-meta-schema": "^0.5.17",
|
|
59
|
+
"graphile-plugin-connection-filter": "^2.6.17",
|
|
60
|
+
"graphile-plugin-connection-filter-postgis": "^1.3.17",
|
|
61
|
+
"graphile-plugin-fulltext-filter": "^2.3.17",
|
|
62
62
|
"graphile-query": "^2.4.7",
|
|
63
|
-
"graphile-search-plugin": "^0.4.
|
|
64
|
-
"graphile-settings": "^2.12.
|
|
65
|
-
"graphile-simple-inflector": "^0.4.
|
|
63
|
+
"graphile-search-plugin": "^0.4.18",
|
|
64
|
+
"graphile-settings": "^2.12.18",
|
|
65
|
+
"graphile-simple-inflector": "^0.4.18",
|
|
66
66
|
"graphile-utils": "^4.14.1",
|
|
67
67
|
"graphql": "15.10.1",
|
|
68
68
|
"graphql-tag": "2.12.6",
|
|
69
69
|
"graphql-upload": "^13.0.0",
|
|
70
70
|
"lru-cache": "^11.2.4",
|
|
71
71
|
"pg": "^8.16.3",
|
|
72
|
-
"pg-cache": "^1.6.
|
|
72
|
+
"pg-cache": "^1.6.16",
|
|
73
73
|
"pg-query-context": "^2.3.7",
|
|
74
74
|
"postgraphile": "^4.14.1",
|
|
75
75
|
"request-ip": "^3.3.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@aws-sdk/client-s3": "^3.958.0",
|
|
79
|
-
"@constructive-io/graphql-codegen": "2.
|
|
79
|
+
"@constructive-io/graphql-codegen": "2.26.0",
|
|
80
80
|
"@types/cors": "^2.8.17",
|
|
81
81
|
"@types/express": "^5.0.6",
|
|
82
82
|
"@types/graphql-upload": "^8.0.12",
|
|
83
83
|
"@types/pg": "^8.16.0",
|
|
84
84
|
"@types/request-ip": "^0.0.41",
|
|
85
|
-
"graphile-test": "2.13.
|
|
85
|
+
"graphile-test": "2.13.17",
|
|
86
86
|
"makage": "^0.1.10",
|
|
87
87
|
"nodemon": "^3.1.10",
|
|
88
88
|
"ts-node": "^10.9.2"
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "b7abf2fdaf0a827d79a80c9c0a29e5c960f227df"
|
|
91
91
|
}
|
package/server.d.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { PgpmOptions } from '@pgpmjs/types';
|
|
2
|
+
import type { Server as HttpServer } from 'http';
|
|
2
3
|
import { Pool, PoolClient } from 'pg';
|
|
3
4
|
export declare const GraphQLServer: (rawOpts?: PgpmOptions) => void;
|
|
4
5
|
declare class Server {
|
|
5
6
|
private app;
|
|
6
7
|
private opts;
|
|
8
|
+
private listenClient;
|
|
9
|
+
private listenRelease;
|
|
10
|
+
private shuttingDown;
|
|
11
|
+
private closed;
|
|
12
|
+
private httpServer;
|
|
7
13
|
constructor(opts: PgpmOptions);
|
|
8
|
-
listen():
|
|
14
|
+
listen(): HttpServer;
|
|
9
15
|
flush(databaseId: string): Promise<void>;
|
|
10
16
|
getPool(): Pool;
|
|
11
17
|
addEventListener(): void;
|
|
12
18
|
listenForChanges(err: Error | null, client: PoolClient, release: () => void): void;
|
|
19
|
+
removeEventListener(): Promise<void>;
|
|
20
|
+
close(opts?: {
|
|
21
|
+
closeCaches?: boolean;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
static closeCaches(opts?: {
|
|
24
|
+
closePools?: boolean;
|
|
25
|
+
}): Promise<void>;
|
|
13
26
|
log(text: string): void;
|
|
14
27
|
error(text: string, err?: unknown): void;
|
|
15
28
|
}
|
package/server.js
CHANGED
|
@@ -12,6 +12,7 @@ const crypto_1 = require("crypto");
|
|
|
12
12
|
const express_1 = __importDefault(require("express"));
|
|
13
13
|
// @ts-ignore
|
|
14
14
|
const graphql_upload_1 = __importDefault(require("graphql-upload"));
|
|
15
|
+
const graphile_cache_1 = require("graphile-cache");
|
|
15
16
|
const pg_cache_1 = require("pg-cache");
|
|
16
17
|
const request_ip_1 = __importDefault(require("request-ip"));
|
|
17
18
|
const api_1 = require("./middleware/api");
|
|
@@ -31,11 +32,17 @@ exports.GraphQLServer = GraphQLServer;
|
|
|
31
32
|
class Server {
|
|
32
33
|
app;
|
|
33
34
|
opts;
|
|
35
|
+
listenClient = null;
|
|
36
|
+
listenRelease = null;
|
|
37
|
+
shuttingDown = false;
|
|
38
|
+
closed = false;
|
|
39
|
+
httpServer = null;
|
|
34
40
|
constructor(opts) {
|
|
35
41
|
this.opts = (0, graphql_env_1.getEnvOptions)(opts);
|
|
42
|
+
const effectiveOpts = this.opts;
|
|
36
43
|
const app = (0, express_1.default)();
|
|
37
|
-
const api = (0, api_1.createApiMiddleware)(
|
|
38
|
-
const authenticate = (0, auth_1.createAuthenticateMiddleware)(
|
|
44
|
+
const api = (0, api_1.createApiMiddleware)(effectiveOpts);
|
|
45
|
+
const authenticate = (0, auth_1.createAuthenticateMiddleware)(effectiveOpts);
|
|
39
46
|
const requestLogger = (req, res, next) => {
|
|
40
47
|
const headerRequestId = req.header('x-request-id');
|
|
41
48
|
const reqId = headerRequestId || (0, crypto_1.randomUUID)();
|
|
@@ -57,13 +64,13 @@ class Server {
|
|
|
57
64
|
};
|
|
58
65
|
// Log startup config in dev mode
|
|
59
66
|
if (isDev()) {
|
|
60
|
-
log.debug(`Database: ${
|
|
61
|
-
log.debug(`Meta schemas: ${
|
|
67
|
+
log.debug(`Database: ${effectiveOpts.pg?.database}@${effectiveOpts.pg?.host}:${effectiveOpts.pg?.port}`);
|
|
68
|
+
log.debug(`Meta schemas: ${effectiveOpts.api?.metaSchemas?.join(', ') || 'default'}`);
|
|
62
69
|
}
|
|
63
70
|
(0, server_utils_1.healthz)(app);
|
|
64
|
-
(0, server_utils_1.trustProxy)(app,
|
|
71
|
+
(0, server_utils_1.trustProxy)(app, effectiveOpts.server.trustProxy);
|
|
65
72
|
// Warn if a global CORS override is set in production
|
|
66
|
-
const fallbackOrigin =
|
|
73
|
+
const fallbackOrigin = effectiveOpts.server?.origin?.trim();
|
|
67
74
|
if (fallbackOrigin && process.env.NODE_ENV === 'production') {
|
|
68
75
|
if (fallbackOrigin === '*') {
|
|
69
76
|
log.warn('CORS wildcard ("*") is enabled in production; this effectively disables CORS and is not recommended. Prefer per-API CORS via meta schema.');
|
|
@@ -80,7 +87,7 @@ class Server {
|
|
|
80
87
|
app.use(requestLogger);
|
|
81
88
|
app.use(api);
|
|
82
89
|
app.use(authenticate);
|
|
83
|
-
app.use((0, graphile_1.graphile)(
|
|
90
|
+
app.use((0, graphile_1.graphile)(effectiveOpts));
|
|
84
91
|
app.use(flush_1.flush);
|
|
85
92
|
this.app = app;
|
|
86
93
|
}
|
|
@@ -96,6 +103,8 @@ class Server {
|
|
|
96
103
|
}
|
|
97
104
|
throw err;
|
|
98
105
|
});
|
|
106
|
+
this.httpServer = httpServer;
|
|
107
|
+
return httpServer;
|
|
99
108
|
}
|
|
100
109
|
async flush(databaseId) {
|
|
101
110
|
await (0, flush_1.flushService)(this.opts, databaseId);
|
|
@@ -104,15 +113,25 @@ class Server {
|
|
|
104
113
|
return (0, pg_cache_1.getPgPool)(this.opts.pg);
|
|
105
114
|
}
|
|
106
115
|
addEventListener() {
|
|
116
|
+
if (this.shuttingDown)
|
|
117
|
+
return;
|
|
107
118
|
const pgPool = this.getPool();
|
|
108
119
|
pgPool.connect(this.listenForChanges.bind(this));
|
|
109
120
|
}
|
|
110
121
|
listenForChanges(err, client, release) {
|
|
111
122
|
if (err) {
|
|
112
123
|
this.error('Error connecting with notify listener', err);
|
|
113
|
-
|
|
124
|
+
if (!this.shuttingDown) {
|
|
125
|
+
setTimeout(() => this.addEventListener(), 5000);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (this.shuttingDown) {
|
|
130
|
+
release();
|
|
114
131
|
return;
|
|
115
132
|
}
|
|
133
|
+
this.listenClient = client;
|
|
134
|
+
this.listenRelease = release;
|
|
116
135
|
client.on('notification', ({ channel, payload }) => {
|
|
117
136
|
if (channel === 'schema:update' && payload) {
|
|
118
137
|
log.info('schema:update', payload);
|
|
@@ -121,12 +140,60 @@ class Server {
|
|
|
121
140
|
});
|
|
122
141
|
client.query('LISTEN "schema:update"');
|
|
123
142
|
client.on('error', (e) => {
|
|
143
|
+
if (this.shuttingDown) {
|
|
144
|
+
release();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
124
147
|
this.error('Error with database notify listener', e);
|
|
125
148
|
release();
|
|
126
149
|
this.addEventListener();
|
|
127
150
|
});
|
|
128
151
|
this.log('connected and listening for changes...');
|
|
129
152
|
}
|
|
153
|
+
async removeEventListener() {
|
|
154
|
+
if (!this.listenClient || !this.listenRelease) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const client = this.listenClient;
|
|
158
|
+
const release = this.listenRelease;
|
|
159
|
+
this.listenClient = null;
|
|
160
|
+
this.listenRelease = null;
|
|
161
|
+
client.removeAllListeners('notification');
|
|
162
|
+
client.removeAllListeners('error');
|
|
163
|
+
try {
|
|
164
|
+
await client.query('UNLISTEN "schema:update"');
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Ignore listener cleanup errors during shutdown.
|
|
168
|
+
}
|
|
169
|
+
release();
|
|
170
|
+
}
|
|
171
|
+
async close(opts = {}) {
|
|
172
|
+
const { closeCaches = false } = opts;
|
|
173
|
+
if (this.closed) {
|
|
174
|
+
if (closeCaches) {
|
|
175
|
+
await Server.closeCaches({ closePools: true });
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.closed = true;
|
|
180
|
+
this.shuttingDown = true;
|
|
181
|
+
await this.removeEventListener();
|
|
182
|
+
if (this.httpServer?.listening) {
|
|
183
|
+
await new Promise((resolve) => this.httpServer.close(() => resolve()));
|
|
184
|
+
}
|
|
185
|
+
if (closeCaches) {
|
|
186
|
+
await Server.closeCaches({ closePools: true });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
static async closeCaches(opts = {}) {
|
|
190
|
+
const { closePools = false } = opts;
|
|
191
|
+
server_utils_1.svcCache.clear();
|
|
192
|
+
graphile_cache_1.graphileCache.clear();
|
|
193
|
+
if (closePools) {
|
|
194
|
+
await pg_cache_1.pgCache.close();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
130
197
|
log(text) {
|
|
131
198
|
log.info(text);
|
|
132
199
|
}
|