@constructive-io/graphql-server 2.16.4 → 2.18.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 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 { getPgPool } from 'pg-cache';
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(opts);
31
- const authenticate = createAuthenticateMiddleware(opts);
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: ${opts.pg?.database}@${opts.pg?.host}:${opts.pg?.port}`);
54
- log.debug(`Meta schemas: ${opts.api?.metaSchemas?.join(', ') || 'default'}`);
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, opts.server.trustProxy);
64
+ trustProxy(app, effectiveOpts.server.trustProxy);
58
65
  // Warn if a global CORS override is set in production
59
- const fallbackOrigin = opts.server?.origin?.trim();
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(opts));
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
- setTimeout(() => this.addEventListener(), 5000);
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.16.4",
3
+ "version": "2.18.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.19",
45
- "@constructive-io/graphql-types": "^2.12.13",
46
- "@constructive-io/s3-utils": "^2.4.1",
47
- "@constructive-io/upload-names": "^2.3.6",
48
- "@constructive-io/url-domains": "^2.3.7",
44
+ "@constructive-io/graphql-env": "^2.9.0",
45
+ "@constructive-io/graphql-types": "^2.13.0",
46
+ "@constructive-io/s3-utils": "^2.5.0",
47
+ "@constructive-io/upload-names": "^2.4.0",
48
+ "@constructive-io/url-domains": "^2.4.0",
49
49
  "@graphile-contrib/pg-many-to-many": "^1.0.2",
50
- "@pgpmjs/logger": "^1.3.7",
51
- "@pgpmjs/server-utils": "^2.8.15",
52
- "@pgpmjs/types": "^2.14.1",
50
+ "@pgpmjs/logger": "^1.4.0",
51
+ "@pgpmjs/server-utils": "^2.9.0",
52
+ "@pgpmjs/types": "^2.15.0",
53
53
  "cors": "^2.8.5",
54
54
  "express": "^5.2.1",
55
55
  "graphile-build": "^4.14.1",
56
- "graphile-cache": "^1.6.15",
57
- "graphile-i18n": "^0.4.16",
58
- "graphile-meta-schema": "^0.5.16",
59
- "graphile-plugin-connection-filter": "^2.6.16",
60
- "graphile-plugin-connection-filter-postgis": "^1.3.16",
61
- "graphile-plugin-fulltext-filter": "^2.3.16",
62
- "graphile-query": "^2.4.7",
63
- "graphile-search-plugin": "^0.4.17",
64
- "graphile-settings": "^2.12.17",
65
- "graphile-simple-inflector": "^0.4.17",
56
+ "graphile-cache": "^1.7.0",
57
+ "graphile-i18n": "^0.5.0",
58
+ "graphile-meta-schema": "^0.6.0",
59
+ "graphile-plugin-connection-filter": "^2.7.0",
60
+ "graphile-plugin-connection-filter-postgis": "^1.4.0",
61
+ "graphile-plugin-fulltext-filter": "^2.4.0",
62
+ "graphile-query": "^2.5.0",
63
+ "graphile-search-plugin": "^0.5.0",
64
+ "graphile-settings": "^2.13.0",
65
+ "graphile-simple-inflector": "^0.5.0",
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.15",
73
- "pg-query-context": "^2.3.7",
72
+ "pg-cache": "^1.7.0",
73
+ "pg-query-context": "^2.4.0",
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.24.1",
79
+ "@constructive-io/graphql-codegen": "2.27.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.16",
85
+ "graphile-test": "2.14.0",
86
86
  "makage": "^0.1.10",
87
87
  "nodemon": "^3.1.10",
88
88
  "ts-node": "^10.9.2"
89
89
  },
90
- "gitHead": "3e08dd946ec5eab57c296e8df180f1dcb1d06514"
90
+ "gitHead": "481b3a50b4eec2da6b376c4cd1868065e1e28edb"
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(): void;
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)(opts);
38
- const authenticate = (0, auth_1.createAuthenticateMiddleware)(opts);
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: ${opts.pg?.database}@${opts.pg?.host}:${opts.pg?.port}`);
61
- log.debug(`Meta schemas: ${opts.api?.metaSchemas?.join(', ') || 'default'}`);
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, opts.server.trustProxy);
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 = opts.server?.origin?.trim();
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)(opts));
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
- setTimeout(() => this.addEventListener(), 5000);
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
  }