@constructive-io/graphql-server 5.34.6 → 5.35.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.md +0 -2
- package/esm/middleware/request-protection.js +1 -8
- package/esm/server.js +0 -8
- package/middleware/request-protection.js +1 -8
- package/package.json +5 -7
- package/server.js +0 -8
- package/esm/middleware/multipart-bridge.js +0 -30
- package/middleware/multipart-bridge.d.ts +0 -26
- package/middleware/multipart-bridge.js +0 -34
package/README.md
CHANGED
|
@@ -70,7 +70,6 @@ Runs an Express server that wires CORS, uploads, domain parsing, auth, and PostG
|
|
|
70
70
|
- Automatic GraphQL API generation from PostgreSQL schemas
|
|
71
71
|
- RLS-aware authentication and per-request `pgSettings`
|
|
72
72
|
- Meta-schema routing by domain + subdomain
|
|
73
|
-
- File uploads via `graphql-upload`
|
|
74
73
|
- GraphiQL and health check endpoints
|
|
75
74
|
- Schema cache flush via `/flush` or database notifications
|
|
76
75
|
- Opt-in observability for memory, DB activity, and Graphile build debugging
|
|
@@ -101,7 +100,6 @@ For the operational workflow, sampler output, and heap snapshot usage, see [docs
|
|
|
101
100
|
- `GET /healthz` -> health check
|
|
102
101
|
- `GET /graphiql` -> GraphiQL UI
|
|
103
102
|
- `GET /graphql` / `POST /graphql` -> GraphQL endpoint
|
|
104
|
-
- `POST /graphql` (multipart) -> file uploads
|
|
105
103
|
- `POST /flush` -> clears cached Graphile schema for the current API; mounted only when `API_FLUSH_TOKEN` is set, and requires `Authorization: Bearer $API_FLUSH_TOKEN`. Without the variable the route is not served (404) — operators deploying a schema-cache flush must set it on the server and on every caller. The `LISTEN/NOTIFY` invalidation path is unaffected.
|
|
106
104
|
- `GET /debug/memory` -> memory/process/Graphile debug snapshot when observability is enabled
|
|
107
105
|
- `GET /debug/db` -> PostgreSQL activity/locks/pool debug snapshot when observability is enabled
|
|
@@ -16,12 +16,6 @@ const resolveProtection = async (req) => {
|
|
|
16
16
|
const resolved = await req.constructive?.useModule('requestProtection');
|
|
17
17
|
return resolved ?? DEFAULT_REQUEST_PROTECTION;
|
|
18
18
|
};
|
|
19
|
-
/**
|
|
20
|
-
* Multipart requests carry file bodies, which are streamed by the upload
|
|
21
|
-
* plugin and bounded by the upload limits rather than by the GraphQL request
|
|
22
|
-
* size: applying a JSON-sized cap to them would reject every upload.
|
|
23
|
-
*/
|
|
24
|
-
const isMultipart = (req) => (req.get('content-type') ?? '').toLowerCase().startsWith('multipart/form-data');
|
|
25
19
|
/**
|
|
26
20
|
* Express middleware that resolves request protection and enforces the one
|
|
27
21
|
* bound that has to be checked before the body is read.
|
|
@@ -50,8 +44,7 @@ export const createRequestProtectionMiddleware = () => {
|
|
|
50
44
|
req.requestProtection = protection;
|
|
51
45
|
const declaredLength = Number(req.get('content-length') ?? '');
|
|
52
46
|
if (Number.isFinite(declaredLength) &&
|
|
53
|
-
declaredLength > protection.maxRequestBytes
|
|
54
|
-
!isMultipart(req)) {
|
|
47
|
+
declaredLength > protection.maxRequestBytes) {
|
|
55
48
|
recordRefusal(req, 'request_too_large');
|
|
56
49
|
respondWithGraphQLError(res, errors.REQUEST_TOO_LARGE({ bytes: declaredLength, limit: protection.maxRequestBytes }));
|
|
57
50
|
return;
|
package/esm/server.js
CHANGED
|
@@ -7,7 +7,6 @@ import { healthz, poweredBy, svcCache, trustProxy } from '@pgpmjs/server-utils';
|
|
|
7
7
|
import cookieParser from 'cookie-parser';
|
|
8
8
|
import express from 'express';
|
|
9
9
|
import { closeAllCaches, graphileCache } from 'graphile-cache';
|
|
10
|
-
import graphqlUpload from 'graphql-upload';
|
|
11
10
|
import { getPgPool } from 'pg-cache';
|
|
12
11
|
import { createAgenticRouter } from './agentic';
|
|
13
12
|
import { closeDebugDatabasePools } from './diagnostics/debug-db-snapshot';
|
|
@@ -26,7 +25,6 @@ import { favicon } from './middleware/favicon';
|
|
|
26
25
|
import { createFlushMiddleware, flushService } from './middleware/flush';
|
|
27
26
|
import { createFnRouter } from './middleware/fn';
|
|
28
27
|
import { graphile } from './middleware/graphile';
|
|
29
|
-
import { multipartBridge } from './middleware/multipart-bridge';
|
|
30
28
|
import { createDebugDatabaseMiddleware } from './middleware/observability/debug-db';
|
|
31
29
|
import { debugMemory } from './middleware/observability/debug-memory';
|
|
32
30
|
import { localObservabilityOnly } from './middleware/observability/guard';
|
|
@@ -128,12 +126,6 @@ class Server {
|
|
|
128
126
|
app.use(poweredBy('constructive'));
|
|
129
127
|
app.use(cookieParser());
|
|
130
128
|
app.use(cors(fallbackOrigin));
|
|
131
|
-
app.use('/graphql', graphqlUpload.graphqlUploadExpress({
|
|
132
|
-
maxFileSize: 10 * 1024 * 1024, // 10 MB
|
|
133
|
-
maxFiles: 10
|
|
134
|
-
}));
|
|
135
|
-
// Rewrite Content-Type after graphql-upload so grafserv accepts the request
|
|
136
|
-
app.use('/graphql', multipartBridge);
|
|
137
129
|
app.use(parseDomains());
|
|
138
130
|
app.use(clientIpMiddleware);
|
|
139
131
|
app.use(requestIdMiddleware());
|
|
@@ -19,12 +19,6 @@ const resolveProtection = async (req) => {
|
|
|
19
19
|
const resolved = await req.constructive?.useModule('requestProtection');
|
|
20
20
|
return resolved ?? express_context_1.DEFAULT_REQUEST_PROTECTION;
|
|
21
21
|
};
|
|
22
|
-
/**
|
|
23
|
-
* Multipart requests carry file bodies, which are streamed by the upload
|
|
24
|
-
* plugin and bounded by the upload limits rather than by the GraphQL request
|
|
25
|
-
* size: applying a JSON-sized cap to them would reject every upload.
|
|
26
|
-
*/
|
|
27
|
-
const isMultipart = (req) => (req.get('content-type') ?? '').toLowerCase().startsWith('multipart/form-data');
|
|
28
22
|
/**
|
|
29
23
|
* Express middleware that resolves request protection and enforces the one
|
|
30
24
|
* bound that has to be checked before the body is read.
|
|
@@ -53,8 +47,7 @@ const createRequestProtectionMiddleware = () => {
|
|
|
53
47
|
req.requestProtection = protection;
|
|
54
48
|
const declaredLength = Number(req.get('content-length') ?? '');
|
|
55
49
|
if (Number.isFinite(declaredLength) &&
|
|
56
|
-
declaredLength > protection.maxRequestBytes
|
|
57
|
-
!isMultipart(req)) {
|
|
50
|
+
declaredLength > protection.maxRequestBytes) {
|
|
58
51
|
(0, recorder_1.recordRefusal)(req, 'request_too_large');
|
|
59
52
|
(0, graphql_response_1.respondWithGraphQLError)(res, errors_1.errors.REQUEST_TOO_LARGE({ bytes: declaredLength, limit: protection.maxRequestBytes }));
|
|
60
53
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.35.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Constructive GraphQL Server",
|
|
6
6
|
"main": "index.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@agentic-kit/ollama": "2.16.0",
|
|
44
44
|
"@constructive-io/csrf": "^0.31.0",
|
|
45
45
|
"@constructive-io/errors": "^0.25.0",
|
|
46
|
-
"@constructive-io/express-context": "^0.
|
|
46
|
+
"@constructive-io/express-context": "^0.37.0",
|
|
47
47
|
"@constructive-io/graphql-env": "^3.37.2",
|
|
48
48
|
"@constructive-io/graphql-types": "^3.36.1",
|
|
49
49
|
"@constructive-io/llm-env": "^0.16.0",
|
|
@@ -71,12 +71,11 @@
|
|
|
71
71
|
"graphile-function-bindings": "^1.19.2",
|
|
72
72
|
"graphile-function-source": "^0.3.2",
|
|
73
73
|
"graphile-plugin-utils": "^1.14.1",
|
|
74
|
-
"graphile-presigned-url-plugin": "^1.
|
|
74
|
+
"graphile-presigned-url-plugin": "^1.24.0",
|
|
75
75
|
"graphile-scoped-introspection": "^0.6.2",
|
|
76
|
-
"graphile-settings": "^6.
|
|
76
|
+
"graphile-settings": "^6.31.0",
|
|
77
77
|
"graphile-utils": "5.0.3",
|
|
78
78
|
"graphql": "16.13.0",
|
|
79
|
-
"graphql-upload": "^13.0.0",
|
|
80
79
|
"lru-cache": "^11.2.7",
|
|
81
80
|
"pg": "^8.21.0",
|
|
82
81
|
"pg-cache": "^3.30.0",
|
|
@@ -90,7 +89,6 @@
|
|
|
90
89
|
"@types/cookie-parser": "^1.4.10",
|
|
91
90
|
"@types/cors": "^2.8.17",
|
|
92
91
|
"@types/express": "^5.0.6",
|
|
93
|
-
"@types/graphql-upload": "^8.0.12",
|
|
94
92
|
"@types/pg": "^8.20.4",
|
|
95
93
|
"@types/supertest": "^7.2.1",
|
|
96
94
|
"graphile-test": "5.19.2",
|
|
@@ -99,5 +97,5 @@
|
|
|
99
97
|
"supertest": "^7.2.2",
|
|
100
98
|
"ts-node": "^10.9.2"
|
|
101
99
|
},
|
|
102
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "b579521f138b5f9a2922ed1066e2ad5dfe2a7073"
|
|
103
101
|
}
|
package/server.js
CHANGED
|
@@ -13,7 +13,6 @@ const server_utils_1 = require("@pgpmjs/server-utils");
|
|
|
13
13
|
const cookie_parser_1 = __importDefault(require("cookie-parser"));
|
|
14
14
|
const express_1 = __importDefault(require("express"));
|
|
15
15
|
const graphile_cache_1 = require("graphile-cache");
|
|
16
|
-
const graphql_upload_1 = __importDefault(require("graphql-upload"));
|
|
17
16
|
const pg_cache_1 = require("pg-cache");
|
|
18
17
|
const agentic_1 = require("./agentic");
|
|
19
18
|
const debug_db_snapshot_1 = require("./diagnostics/debug-db-snapshot");
|
|
@@ -32,7 +31,6 @@ const favicon_1 = require("./middleware/favicon");
|
|
|
32
31
|
const flush_1 = require("./middleware/flush");
|
|
33
32
|
const fn_1 = require("./middleware/fn");
|
|
34
33
|
const graphile_1 = require("./middleware/graphile");
|
|
35
|
-
const multipart_bridge_1 = require("./middleware/multipart-bridge");
|
|
36
34
|
const debug_db_1 = require("./middleware/observability/debug-db");
|
|
37
35
|
const debug_memory_1 = require("./middleware/observability/debug-memory");
|
|
38
36
|
const guard_1 = require("./middleware/observability/guard");
|
|
@@ -135,12 +133,6 @@ class Server {
|
|
|
135
133
|
app.use((0, server_utils_1.poweredBy)('constructive'));
|
|
136
134
|
app.use((0, cookie_parser_1.default)());
|
|
137
135
|
app.use((0, cors_1.cors)(fallbackOrigin));
|
|
138
|
-
app.use('/graphql', graphql_upload_1.default.graphqlUploadExpress({
|
|
139
|
-
maxFileSize: 10 * 1024 * 1024, // 10 MB
|
|
140
|
-
maxFiles: 10
|
|
141
|
-
}));
|
|
142
|
-
// Rewrite Content-Type after graphql-upload so grafserv accepts the request
|
|
143
|
-
app.use('/graphql', multipart_bridge_1.multipartBridge);
|
|
144
136
|
app.use((0, url_domains_1.middleware)());
|
|
145
137
|
app.use(client_ip_1.clientIpMiddleware);
|
|
146
138
|
app.use((0, express_context_1.requestIdMiddleware)());
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bridge middleware between graphql-upload and grafserv (PostGraphile v5).
|
|
3
|
-
*
|
|
4
|
-
* graphql-upload's `graphqlUploadExpress()` parses multipart/form-data requests
|
|
5
|
-
* and sets `req.body` to a standard GraphQL operation object, but leaves the
|
|
6
|
-
* original Content-Type header unchanged. grafserv rejects any Content-Type it
|
|
7
|
-
* doesn't recognize (415 Unsupported Media Type) because its allowlist only
|
|
8
|
-
* includes `application/json` and `application/graphql`.
|
|
9
|
-
*
|
|
10
|
-
* This middleware rewrites the Content-Type header to `application/json` after
|
|
11
|
-
* graphql-upload has finished parsing, so grafserv accepts the request.
|
|
12
|
-
*
|
|
13
|
-
* This is the standard bridge pattern for grafserv + graphql-upload.
|
|
14
|
-
* See: https://github.com/graphql/graphql-http/discussions/36
|
|
15
|
-
*
|
|
16
|
-
* Must be registered immediately after `graphqlUploadExpress()` in the
|
|
17
|
-
* middleware chain — it relies on graphql-upload having already parsed the
|
|
18
|
-
* multipart body into `req.body`.
|
|
19
|
-
*
|
|
20
|
-
* Security note: multipart/form-data is a CORS "simple request" content type,
|
|
21
|
-
* which means browsers send it without a preflight OPTIONS check. The CSRF risk
|
|
22
|
-
* is mitigated by the CORS middleware that runs before this middleware in the
|
|
23
|
-
* Express pipeline.
|
|
24
|
-
*/
|
|
25
|
-
export const multipartBridge = (req, _res, next) => {
|
|
26
|
-
if (req.body != null && req.headers['content-type']?.startsWith('multipart/form-data')) {
|
|
27
|
-
req.headers['content-type'] = 'application/json';
|
|
28
|
-
}
|
|
29
|
-
next();
|
|
30
|
-
};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { RequestHandler } from 'express';
|
|
2
|
-
/**
|
|
3
|
-
* Bridge middleware between graphql-upload and grafserv (PostGraphile v5).
|
|
4
|
-
*
|
|
5
|
-
* graphql-upload's `graphqlUploadExpress()` parses multipart/form-data requests
|
|
6
|
-
* and sets `req.body` to a standard GraphQL operation object, but leaves the
|
|
7
|
-
* original Content-Type header unchanged. grafserv rejects any Content-Type it
|
|
8
|
-
* doesn't recognize (415 Unsupported Media Type) because its allowlist only
|
|
9
|
-
* includes `application/json` and `application/graphql`.
|
|
10
|
-
*
|
|
11
|
-
* This middleware rewrites the Content-Type header to `application/json` after
|
|
12
|
-
* graphql-upload has finished parsing, so grafserv accepts the request.
|
|
13
|
-
*
|
|
14
|
-
* This is the standard bridge pattern for grafserv + graphql-upload.
|
|
15
|
-
* See: https://github.com/graphql/graphql-http/discussions/36
|
|
16
|
-
*
|
|
17
|
-
* Must be registered immediately after `graphqlUploadExpress()` in the
|
|
18
|
-
* middleware chain — it relies on graphql-upload having already parsed the
|
|
19
|
-
* multipart body into `req.body`.
|
|
20
|
-
*
|
|
21
|
-
* Security note: multipart/form-data is a CORS "simple request" content type,
|
|
22
|
-
* which means browsers send it without a preflight OPTIONS check. The CSRF risk
|
|
23
|
-
* is mitigated by the CORS middleware that runs before this middleware in the
|
|
24
|
-
* Express pipeline.
|
|
25
|
-
*/
|
|
26
|
-
export declare const multipartBridge: RequestHandler;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.multipartBridge = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Bridge middleware between graphql-upload and grafserv (PostGraphile v5).
|
|
6
|
-
*
|
|
7
|
-
* graphql-upload's `graphqlUploadExpress()` parses multipart/form-data requests
|
|
8
|
-
* and sets `req.body` to a standard GraphQL operation object, but leaves the
|
|
9
|
-
* original Content-Type header unchanged. grafserv rejects any Content-Type it
|
|
10
|
-
* doesn't recognize (415 Unsupported Media Type) because its allowlist only
|
|
11
|
-
* includes `application/json` and `application/graphql`.
|
|
12
|
-
*
|
|
13
|
-
* This middleware rewrites the Content-Type header to `application/json` after
|
|
14
|
-
* graphql-upload has finished parsing, so grafserv accepts the request.
|
|
15
|
-
*
|
|
16
|
-
* This is the standard bridge pattern for grafserv + graphql-upload.
|
|
17
|
-
* See: https://github.com/graphql/graphql-http/discussions/36
|
|
18
|
-
*
|
|
19
|
-
* Must be registered immediately after `graphqlUploadExpress()` in the
|
|
20
|
-
* middleware chain — it relies on graphql-upload having already parsed the
|
|
21
|
-
* multipart body into `req.body`.
|
|
22
|
-
*
|
|
23
|
-
* Security note: multipart/form-data is a CORS "simple request" content type,
|
|
24
|
-
* which means browsers send it without a preflight OPTIONS check. The CSRF risk
|
|
25
|
-
* is mitigated by the CORS middleware that runs before this middleware in the
|
|
26
|
-
* Express pipeline.
|
|
27
|
-
*/
|
|
28
|
-
const multipartBridge = (req, _res, next) => {
|
|
29
|
-
if (req.body != null && req.headers['content-type']?.startsWith('multipart/form-data')) {
|
|
30
|
-
req.headers['content-type'] = 'application/json';
|
|
31
|
-
}
|
|
32
|
-
next();
|
|
33
|
-
};
|
|
34
|
-
exports.multipartBridge = multipartBridge;
|