@constructive-io/graphql-server 3.1.1 → 4.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/errors/404-message.js +1 -1
- package/errors/api-errors.d.ts +200 -0
- package/errors/api-errors.js +276 -0
- package/esm/errors/404-message.js +1 -1
- package/esm/errors/api-errors.js +261 -0
- package/esm/index.js +2 -0
- package/esm/middleware/api.js +355 -277
- package/esm/middleware/auth.js +25 -7
- package/esm/middleware/error-handler.js +86 -0
- package/esm/middleware/favicon.js +12 -0
- package/esm/middleware/graphile.js +149 -64
- package/esm/options.js +232 -0
- package/esm/schema.js +24 -11
- package/esm/server.js +41 -5
- package/index.d.ts +1 -0
- package/index.js +2 -0
- package/middleware/api.d.ts +3 -15
- package/middleware/api.js +359 -283
- package/middleware/auth.js +25 -7
- package/middleware/error-handler.d.ts +4 -0
- package/middleware/error-handler.js +94 -0
- package/middleware/favicon.d.ts +2 -0
- package/middleware/favicon.js +16 -0
- package/middleware/graphile.d.ts +14 -0
- package/middleware/graphile.js +149 -64
- package/options.d.ts +131 -0
- package/options.js +244 -0
- package/package.json +23 -24
- package/schema.d.ts +2 -2
- package/schema.js +23 -10
- package/server.d.ts +24 -2
- package/server.js +39 -3
- package/codegen/orm/client.d.ts +0 -55
- package/codegen/orm/client.js +0 -75
- package/codegen/orm/index.d.ts +0 -36
- package/codegen/orm/index.js +0 -59
- package/codegen/orm/input-types.d.ts +0 -20140
- package/codegen/orm/input-types.js +0 -2
- package/codegen/orm/models/api.d.ts +0 -42
- package/codegen/orm/models/api.js +0 -76
- package/codegen/orm/models/domain.d.ts +0 -42
- package/codegen/orm/models/domain.js +0 -76
- package/codegen/orm/models/index.d.ts +0 -7
- package/codegen/orm/models/index.js +0 -12
- package/codegen/orm/mutation/index.d.ts +0 -7
- package/codegen/orm/mutation/index.js +0 -7
- package/codegen/orm/query/index.d.ts +0 -20
- package/codegen/orm/query/index.js +0 -24
- package/codegen/orm/query-builder.d.ts +0 -81
- package/codegen/orm/query-builder.js +0 -496
- package/codegen/orm/select-types.d.ts +0 -83
- package/codegen/orm/select-types.js +0 -7
- package/codegen/orm/types.d.ts +0 -6
- package/codegen/orm/types.js +0 -23
- package/esm/codegen/orm/client.js +0 -70
- package/esm/codegen/orm/index.js +0 -39
- package/esm/codegen/orm/input-types.js +0 -1
- package/esm/codegen/orm/models/api.js +0 -72
- package/esm/codegen/orm/models/domain.js +0 -72
- package/esm/codegen/orm/models/index.js +0 -7
- package/esm/codegen/orm/mutation/index.js +0 -4
- package/esm/codegen/orm/query/index.js +0 -21
- package/esm/codegen/orm/query-builder.js +0 -452
- package/esm/codegen/orm/select-types.js +0 -6
- package/esm/codegen/orm/types.js +0 -7
- package/esm/middleware/gql.js +0 -116
- package/esm/plugins/PublicKeySignature.js +0 -114
- package/esm/scripts/codegen-schema.js +0 -71
- package/esm/scripts/create-bucket.js +0 -40
- package/middleware/gql.d.ts +0 -164
- package/middleware/gql.js +0 -121
- package/plugins/PublicKeySignature.d.ts +0 -11
- package/plugins/PublicKeySignature.js +0 -121
- package/scripts/codegen-schema.d.ts +0 -1
- package/scripts/codegen-schema.js +0 -76
- package/scripts/create-bucket.d.ts +0 -1
- package/scripts/create-bucket.js +0 -42
package/middleware/auth.js
CHANGED
|
@@ -14,6 +14,7 @@ const isDev = () => (0, graphql_env_1.getNodeEnv)() === 'development';
|
|
|
14
14
|
const createAuthenticateMiddleware = (opts) => {
|
|
15
15
|
return async (req, res, next) => {
|
|
16
16
|
const api = req.api;
|
|
17
|
+
log.info(`[auth] middleware called, api=${api ? 'present' : 'missing'}`);
|
|
17
18
|
if (!api) {
|
|
18
19
|
res.status(500).send('Missing API info');
|
|
19
20
|
return;
|
|
@@ -23,19 +24,26 @@ const createAuthenticateMiddleware = (opts) => {
|
|
|
23
24
|
database: api.dbname,
|
|
24
25
|
});
|
|
25
26
|
const rlsModule = api.rlsModule;
|
|
27
|
+
log.info(`[auth] rlsModule=${rlsModule ? 'present' : 'missing'}, ` +
|
|
28
|
+
`authenticate=${rlsModule?.authenticate ?? 'none'}, ` +
|
|
29
|
+
`authenticateStrict=${rlsModule?.authenticateStrict ?? 'none'}, ` +
|
|
30
|
+
`privateSchema=${rlsModule?.privateSchema?.schemaName ?? 'none'}`);
|
|
26
31
|
if (!rlsModule) {
|
|
27
|
-
|
|
28
|
-
log.debug('No RLS module configured, skipping auth');
|
|
32
|
+
log.info('[auth] No RLS module configured, skipping auth');
|
|
29
33
|
return next();
|
|
30
34
|
}
|
|
31
|
-
const authFn = opts.server
|
|
35
|
+
const authFn = opts.server?.strictAuth
|
|
32
36
|
? rlsModule.authenticateStrict
|
|
33
37
|
: rlsModule.authenticate;
|
|
38
|
+
log.info(`[auth] strictAuth=${opts.server?.strictAuth ?? false}, authFn=${authFn ?? 'none'}`);
|
|
34
39
|
if (authFn && rlsModule.privateSchema.schemaName) {
|
|
35
40
|
const { authorization = '' } = req.headers;
|
|
36
41
|
const [authType, authToken] = authorization.split(' ');
|
|
37
42
|
let token = {};
|
|
43
|
+
log.info(`[auth] authorization header present=${!!authorization}, ` +
|
|
44
|
+
`authType=${authType ?? 'none'}, hasToken=${!!authToken}`);
|
|
38
45
|
if (authType?.toLowerCase() === 'bearer' && authToken) {
|
|
46
|
+
log.info('[auth] Processing bearer token authentication');
|
|
39
47
|
const context = {
|
|
40
48
|
'jwt.claims.ip_address': req.clientIp,
|
|
41
49
|
};
|
|
@@ -45,25 +53,28 @@ const createAuthenticateMiddleware = (opts) => {
|
|
|
45
53
|
if (req.get('User-Agent')) {
|
|
46
54
|
context['jwt.claims.user_agent'] = req.get('User-Agent');
|
|
47
55
|
}
|
|
56
|
+
const authQuery = `SELECT * FROM "${rlsModule.privateSchema.schemaName}"."${authFn}"($1)`;
|
|
57
|
+
log.info(`[auth] Executing auth query: ${authQuery}`);
|
|
48
58
|
try {
|
|
49
59
|
const result = await (0, pg_query_context_1.default)({
|
|
50
60
|
client: pool,
|
|
51
61
|
context,
|
|
52
|
-
query:
|
|
62
|
+
query: authQuery,
|
|
53
63
|
variables: [authToken],
|
|
54
64
|
});
|
|
65
|
+
log.info(`[auth] Query result: rowCount=${result?.rowCount}`);
|
|
55
66
|
if (result?.rowCount === 0) {
|
|
67
|
+
log.info('[auth] No rows returned, returning UNAUTHENTICATED');
|
|
56
68
|
res.status(200).json({
|
|
57
69
|
errors: [{ extensions: { code: 'UNAUTHENTICATED' } }],
|
|
58
70
|
});
|
|
59
71
|
return;
|
|
60
72
|
}
|
|
61
73
|
token = result.rows[0];
|
|
62
|
-
|
|
63
|
-
log.debug(`Auth success: role=${token.role}`);
|
|
74
|
+
log.info(`[auth] Auth success: role=${token.role}, user_id=${token.user_id}`);
|
|
64
75
|
}
|
|
65
76
|
catch (e) {
|
|
66
|
-
log.error('Auth error:', e.message);
|
|
77
|
+
log.error('[auth] Auth error:', e.message);
|
|
67
78
|
res.status(200).json({
|
|
68
79
|
errors: [
|
|
69
80
|
{
|
|
@@ -77,8 +88,15 @@ const createAuthenticateMiddleware = (opts) => {
|
|
|
77
88
|
return;
|
|
78
89
|
}
|
|
79
90
|
}
|
|
91
|
+
else {
|
|
92
|
+
log.info('[auth] No bearer token provided, using anonymous auth');
|
|
93
|
+
}
|
|
80
94
|
req.token = token;
|
|
81
95
|
}
|
|
96
|
+
else {
|
|
97
|
+
log.info(`[auth] Skipping auth: authFn=${authFn ?? 'none'}, ` +
|
|
98
|
+
`privateSchema=${rlsModule.privateSchema?.schemaName ?? 'none'}`);
|
|
99
|
+
}
|
|
82
100
|
next();
|
|
83
101
|
};
|
|
84
102
|
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.notFoundHandler = exports.errorHandler = void 0;
|
|
7
|
+
const graphql_env_1 = require("@constructive-io/graphql-env");
|
|
8
|
+
const logger_1 = require("@pgpmjs/logger");
|
|
9
|
+
const api_errors_1 = require("../errors/api-errors");
|
|
10
|
+
const _404_message_1 = __importDefault(require("../errors/404-message"));
|
|
11
|
+
const _50x_1 = __importDefault(require("../errors/50x"));
|
|
12
|
+
require("./types");
|
|
13
|
+
const log = new logger_1.Logger('error-handler');
|
|
14
|
+
const isDevelopment = () => (0, graphql_env_1.getNodeEnv)() === 'development';
|
|
15
|
+
const wantsJson = (req) => {
|
|
16
|
+
const accept = req.get('Accept') || '';
|
|
17
|
+
return accept.includes('application/json') || accept.includes('application/graphql-response+json');
|
|
18
|
+
};
|
|
19
|
+
const sanitizeMessage = (error) => {
|
|
20
|
+
if (isDevelopment())
|
|
21
|
+
return error.message;
|
|
22
|
+
if ((0, api_errors_1.isApiError)(error))
|
|
23
|
+
return error.message;
|
|
24
|
+
if (error.message?.includes('ECONNREFUSED'))
|
|
25
|
+
return 'Service temporarily unavailable';
|
|
26
|
+
if (error.message?.includes('timeout') || error.message?.includes('ETIMEDOUT'))
|
|
27
|
+
return 'Request timed out';
|
|
28
|
+
if (error.message?.includes('does not exist'))
|
|
29
|
+
return 'The requested resource does not exist';
|
|
30
|
+
return 'An unexpected error occurred';
|
|
31
|
+
};
|
|
32
|
+
const categorizeError = (err) => {
|
|
33
|
+
if ((0, api_errors_1.isApiError)(err)) {
|
|
34
|
+
return {
|
|
35
|
+
statusCode: err.statusCode,
|
|
36
|
+
code: err.code,
|
|
37
|
+
message: sanitizeMessage(err),
|
|
38
|
+
logLevel: err.statusCode >= 500 ? 'error' : 'warn',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (err.message?.includes('ECONNREFUSED') || err.message?.includes('connection terminated')) {
|
|
42
|
+
return { statusCode: 503, code: 'SERVICE_UNAVAILABLE', message: sanitizeMessage(err), logLevel: 'error' };
|
|
43
|
+
}
|
|
44
|
+
if (err.message?.includes('timeout') || err.message?.includes('ETIMEDOUT')) {
|
|
45
|
+
return { statusCode: 504, code: 'GATEWAY_TIMEOUT', message: sanitizeMessage(err), logLevel: 'error' };
|
|
46
|
+
}
|
|
47
|
+
return { statusCode: 500, code: 'INTERNAL_ERROR', message: sanitizeMessage(err), logLevel: 'error' };
|
|
48
|
+
};
|
|
49
|
+
const sendResponse = (req, res, { statusCode, code, message }) => {
|
|
50
|
+
if (wantsJson(req)) {
|
|
51
|
+
res.status(statusCode).json({ error: { code, message, requestId: req.requestId } });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
res.status(statusCode).send(statusCode >= 500 ? _50x_1.default : (0, _404_message_1.default)(message));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const logError = (err, req, level) => {
|
|
58
|
+
const context = {
|
|
59
|
+
requestId: req.requestId,
|
|
60
|
+
path: req.path,
|
|
61
|
+
method: req.method,
|
|
62
|
+
host: req.get('host'),
|
|
63
|
+
databaseId: req.databaseId,
|
|
64
|
+
svcKey: req.svc_key,
|
|
65
|
+
clientIp: req.clientIp,
|
|
66
|
+
};
|
|
67
|
+
if ((0, api_errors_1.isApiError)(err)) {
|
|
68
|
+
log[level]({ event: 'api_error', code: err.code, statusCode: err.statusCode, message: err.message, ...context });
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
log[level]({ event: 'unexpected_error', name: err.name, message: err.message, stack: isDevelopment() ? err.stack : undefined, ...context });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const errorHandler = (err, req, res, _next) => {
|
|
75
|
+
if (res.headersSent) {
|
|
76
|
+
log.warn({ event: 'headers_already_sent', requestId: req.requestId, path: req.path, errorMessage: err.message });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const response = categorizeError(err);
|
|
80
|
+
logError(err, req, response.logLevel);
|
|
81
|
+
sendResponse(req, res, response);
|
|
82
|
+
};
|
|
83
|
+
exports.errorHandler = errorHandler;
|
|
84
|
+
const notFoundHandler = (req, res, _next) => {
|
|
85
|
+
const message = `Route not found: ${req.method} ${req.path}`;
|
|
86
|
+
log.warn({ event: 'route_not_found', path: req.path, method: req.method, requestId: req.requestId });
|
|
87
|
+
if (wantsJson(req)) {
|
|
88
|
+
res.status(404).json({ error: { code: 'NOT_FOUND', message, requestId: req.requestId } });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
res.status(404).send((0, _404_message_1.default)(message));
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
exports.notFoundHandler = notFoundHandler;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.favicon = void 0;
|
|
4
|
+
const FAVICON_BASE64 = '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';
|
|
5
|
+
const faviconBuffer = Buffer.from(FAVICON_BASE64, 'base64');
|
|
6
|
+
const favicon = (req, res, next) => {
|
|
7
|
+
if (req.path === '/favicon.ico' && req.method === 'GET') {
|
|
8
|
+
res.setHeader('Content-Type', 'image/x-icon');
|
|
9
|
+
res.setHeader('Content-Length', faviconBuffer.length);
|
|
10
|
+
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
11
|
+
res.status(200).send(faviconBuffer);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
next();
|
|
15
|
+
};
|
|
16
|
+
exports.favicon = favicon;
|
package/middleware/graphile.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { ConstructiveOptions } from '@constructive-io/graphql-types';
|
|
2
2
|
import { RequestHandler } from 'express';
|
|
3
3
|
import './types';
|
|
4
|
+
/**
|
|
5
|
+
* Returns the number of currently in-flight handler creation operations.
|
|
6
|
+
* Useful for monitoring and debugging.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getInFlightCount(): number;
|
|
9
|
+
/**
|
|
10
|
+
* Returns the cache keys for all currently in-flight handler creation operations.
|
|
11
|
+
* Useful for monitoring and debugging.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getInFlightKeys(): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Clears the in-flight map. Used for testing purposes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function clearInFlightMap(): void;
|
|
4
18
|
export declare const graphile: (opts: ConstructiveOptions) => RequestHandler;
|
package/middleware/graphile.js
CHANGED
|
@@ -1,18 +1,122 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.graphile = void 0;
|
|
4
|
+
exports.getInFlightCount = getInFlightCount;
|
|
5
|
+
exports.getInFlightKeys = getInFlightKeys;
|
|
6
|
+
exports.clearInFlightMap = clearInFlightMap;
|
|
7
7
|
const logger_1 = require("@pgpmjs/logger");
|
|
8
8
|
const graphile_cache_1 = require("graphile-cache");
|
|
9
9
|
const graphile_settings_1 = require("graphile-settings");
|
|
10
10
|
const pg_cache_1 = require("pg-cache");
|
|
11
|
-
const
|
|
11
|
+
const pg_env_1 = require("pg-env");
|
|
12
12
|
require("./types"); // for Request type
|
|
13
|
-
const
|
|
13
|
+
const api_errors_1 = require("../errors/api-errors");
|
|
14
|
+
/**
|
|
15
|
+
* Custom maskError function that always returns the original error.
|
|
16
|
+
*
|
|
17
|
+
* By default, grafserv masks errors for security (hiding sensitive database errors
|
|
18
|
+
* from clients). We disable this masking to show full error messages.
|
|
19
|
+
*
|
|
20
|
+
* Upstream reference:
|
|
21
|
+
* - grafserv defaultMaskError: node_modules/grafserv/dist/options.js
|
|
22
|
+
* - SafeError interface: grafast isSafeError() - errors implementing SafeError
|
|
23
|
+
* are shown as-is even with default masking
|
|
24
|
+
*
|
|
25
|
+
* If you need to restore masking behavior, see the upstream implementation which:
|
|
26
|
+
* 1. Returns GraphQLError instances as-is
|
|
27
|
+
* 2. Returns SafeError instances with their message exposed
|
|
28
|
+
* 3. Masks other errors with a hash/ID and logs the original
|
|
29
|
+
*/
|
|
30
|
+
const maskError = (error) => {
|
|
31
|
+
return error;
|
|
32
|
+
};
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Single-Flight Pattern: In-Flight Tracking
|
|
35
|
+
// =============================================================================
|
|
36
|
+
/**
|
|
37
|
+
* Tracks in-flight handler creation promises to prevent duplicate creations.
|
|
38
|
+
* When multiple concurrent requests arrive for the same cache key, only the
|
|
39
|
+
* first request creates the handler while others wait on the same promise.
|
|
40
|
+
*/
|
|
41
|
+
const creating = new Map();
|
|
42
|
+
/**
|
|
43
|
+
* Returns the number of currently in-flight handler creation operations.
|
|
44
|
+
* Useful for monitoring and debugging.
|
|
45
|
+
*/
|
|
46
|
+
function getInFlightCount() {
|
|
47
|
+
return creating.size;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns the cache keys for all currently in-flight handler creation operations.
|
|
51
|
+
* Useful for monitoring and debugging.
|
|
52
|
+
*/
|
|
53
|
+
function getInFlightKeys() {
|
|
54
|
+
return [...creating.keys()];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Clears the in-flight map. Used for testing purposes.
|
|
58
|
+
*/
|
|
59
|
+
function clearInFlightMap() {
|
|
60
|
+
creating.clear();
|
|
61
|
+
}
|
|
14
62
|
const log = new logger_1.Logger('graphile');
|
|
15
63
|
const reqLabel = (req) => req.requestId ? `[${req.requestId}]` : '[req]';
|
|
64
|
+
/**
|
|
65
|
+
* Build a PostGraphile v5 preset for a tenant
|
|
66
|
+
*/
|
|
67
|
+
const buildPreset = (connectionString, schemas, anonRole, roleName) => ({
|
|
68
|
+
extends: [graphile_settings_1.ConstructivePreset],
|
|
69
|
+
pgServices: [
|
|
70
|
+
(0, graphile_settings_1.makePgService)({
|
|
71
|
+
connectionString,
|
|
72
|
+
schemas,
|
|
73
|
+
}),
|
|
74
|
+
],
|
|
75
|
+
grafserv: {
|
|
76
|
+
graphqlPath: '/graphql',
|
|
77
|
+
graphiqlPath: '/graphiql',
|
|
78
|
+
graphiql: true,
|
|
79
|
+
maskError,
|
|
80
|
+
},
|
|
81
|
+
grafast: {
|
|
82
|
+
explain: process.env.NODE_ENV === 'development',
|
|
83
|
+
context: (requestContext) => {
|
|
84
|
+
// In grafserv/express/v4, the request is available at requestContext.expressv4.req
|
|
85
|
+
const req = requestContext?.expressv4?.req;
|
|
86
|
+
const context = {};
|
|
87
|
+
if (req) {
|
|
88
|
+
if (req.databaseId) {
|
|
89
|
+
context['jwt.claims.database_id'] = req.databaseId;
|
|
90
|
+
}
|
|
91
|
+
if (req.clientIp) {
|
|
92
|
+
context['jwt.claims.ip_address'] = req.clientIp;
|
|
93
|
+
}
|
|
94
|
+
if (req.get('origin')) {
|
|
95
|
+
context['jwt.claims.origin'] = req.get('origin');
|
|
96
|
+
}
|
|
97
|
+
if (req.get('User-Agent')) {
|
|
98
|
+
context['jwt.claims.user_agent'] = req.get('User-Agent');
|
|
99
|
+
}
|
|
100
|
+
if (req.token?.user_id) {
|
|
101
|
+
return {
|
|
102
|
+
pgSettings: {
|
|
103
|
+
role: roleName,
|
|
104
|
+
'jwt.claims.token_id': req.token.id,
|
|
105
|
+
'jwt.claims.user_id': req.token.user_id,
|
|
106
|
+
...context,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
pgSettings: {
|
|
113
|
+
role: anonRole,
|
|
114
|
+
...context,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
16
120
|
const graphile = (opts) => {
|
|
17
121
|
return async (req, res, next) => {
|
|
18
122
|
const label = reqLabel(req);
|
|
@@ -29,76 +133,57 @@ const graphile = (opts) => {
|
|
|
29
133
|
}
|
|
30
134
|
const { dbname, anonRole, roleName, schema } = api;
|
|
31
135
|
const schemaLabel = schema?.join(',') || 'unknown';
|
|
136
|
+
// =========================================================================
|
|
137
|
+
// Phase A: Cache Check (fast path)
|
|
138
|
+
// =========================================================================
|
|
32
139
|
const cached = graphile_cache_1.graphileCache.get(key);
|
|
33
140
|
if (cached) {
|
|
34
141
|
log.debug(`${label} PostGraphile cache hit key=${key} db=${dbname} schemas=${schemaLabel}`);
|
|
35
142
|
return cached.handler(req, res, next);
|
|
36
143
|
}
|
|
37
144
|
log.debug(`${label} PostGraphile cache miss key=${key} db=${dbname} schemas=${schemaLabel}`);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
log.info(`${label} Enabling PublicKeySignature plugin for ${dbname}`);
|
|
48
|
-
options.appendPlugins.push((0, PublicKeySignature_1.default)(pubkey_challenge.data));
|
|
49
|
-
}
|
|
50
|
-
options.appendPlugins = options.appendPlugins ?? [];
|
|
51
|
-
if (opts.graphile?.appendPlugins) {
|
|
52
|
-
options.appendPlugins.push(...opts.graphile.appendPlugins);
|
|
53
|
-
}
|
|
54
|
-
options.pgSettings = async function pgSettings(request) {
|
|
55
|
-
const gqlReq = request;
|
|
56
|
-
const settingsLabel = reqLabel(gqlReq);
|
|
57
|
-
const context = {
|
|
58
|
-
[`jwt.claims.database_id`]: gqlReq.databaseId,
|
|
59
|
-
[`jwt.claims.ip_address`]: gqlReq.clientIp,
|
|
60
|
-
};
|
|
61
|
-
if (gqlReq.get('origin')) {
|
|
62
|
-
context['jwt.claims.origin'] = gqlReq.get('origin');
|
|
145
|
+
// =========================================================================
|
|
146
|
+
// Phase B: In-Flight Check (single-flight coalescing)
|
|
147
|
+
// =========================================================================
|
|
148
|
+
const inFlight = creating.get(key);
|
|
149
|
+
if (inFlight) {
|
|
150
|
+
log.debug(`${label} Coalescing request for PostGraphile[${key}] - waiting for in-flight creation`);
|
|
151
|
+
try {
|
|
152
|
+
const instance = await inFlight;
|
|
153
|
+
return instance.handler(req, res, next);
|
|
63
154
|
}
|
|
64
|
-
|
|
65
|
-
|
|
155
|
+
catch (error) {
|
|
156
|
+
// Re-throw to be caught by outer try-catch
|
|
157
|
+
throw error;
|
|
66
158
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
...context,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
log.debug(`${settingsLabel} pgSettings role=${anonRole} db=${gqlReq.databaseId} ip=${gqlReq.clientIp}`);
|
|
77
|
-
return { role: anonRole, ...context };
|
|
78
|
-
};
|
|
79
|
-
options.graphqlRoute = '/graphql';
|
|
80
|
-
options.graphiqlRoute = '/graphiql';
|
|
81
|
-
options.graphileBuildOptions = {
|
|
82
|
-
...options.graphileBuildOptions,
|
|
83
|
-
...opts.graphile?.graphileBuildOptions,
|
|
84
|
-
};
|
|
85
|
-
const graphileOpts = {
|
|
86
|
-
...options,
|
|
87
|
-
...opts.graphile?.overrideSettings,
|
|
88
|
-
};
|
|
89
|
-
log.info(`${label} Building PostGraphile handler key=${key} db=${dbname} schemas=${schemaLabel} role=${roleName} anon=${anonRole}`);
|
|
90
|
-
const pgPool = (0, pg_cache_1.getPgPool)({
|
|
159
|
+
}
|
|
160
|
+
// =========================================================================
|
|
161
|
+
// Phase C: Create New Handler (first request for this key)
|
|
162
|
+
// =========================================================================
|
|
163
|
+
log.info(`${label} Building PostGraphile v5 handler key=${key} db=${dbname} schemas=${schemaLabel} role=${roleName} anon=${anonRole}`);
|
|
164
|
+
const pgConfig = (0, pg_env_1.getPgEnvOptions)({
|
|
91
165
|
...opts.pg,
|
|
92
166
|
database: dbname,
|
|
93
167
|
});
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
168
|
+
const connectionString = (0, pg_cache_1.buildConnectionString)(pgConfig.user, pgConfig.password, pgConfig.host, pgConfig.port, pgConfig.database);
|
|
169
|
+
// Create promise and store in in-flight map BEFORE try block
|
|
170
|
+
const preset = buildPreset(connectionString, schema || [], anonRole, roleName);
|
|
171
|
+
const creationPromise = (0, graphile_cache_1.createGraphileInstance)({ preset, cacheKey: key });
|
|
172
|
+
creating.set(key, creationPromise);
|
|
173
|
+
try {
|
|
174
|
+
const instance = await creationPromise;
|
|
175
|
+
graphile_cache_1.graphileCache.set(key, instance);
|
|
176
|
+
log.info(`${label} Cached PostGraphile v5 handler key=${key} db=${dbname}`);
|
|
177
|
+
return instance.handler(req, res, next);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
log.error(`${label} Failed to create PostGraphile[${key}]:`, error);
|
|
181
|
+
throw new api_errors_1.HandlerCreationError(`Failed to create handler for ${key}: ${error instanceof Error ? error.message : String(error)}`, { cacheKey: key, cause: error instanceof Error ? error.message : String(error) });
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
// Always clean up in-flight tracker
|
|
185
|
+
creating.delete(key);
|
|
186
|
+
}
|
|
102
187
|
}
|
|
103
188
|
catch (e) {
|
|
104
189
|
log.error(`${label} PostGraphile middleware error`, e);
|
package/options.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL Server Options - Configuration utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides type-safe configuration utilities for the GraphQL server.
|
|
5
|
+
* It includes type guards for runtime validation and utility functions for
|
|
6
|
+
* configuration normalization.
|
|
7
|
+
*
|
|
8
|
+
* The main configuration type is `ConstructiveOptions` from @constructive-io/graphql-types.
|
|
9
|
+
*
|
|
10
|
+
* @module options
|
|
11
|
+
*/
|
|
12
|
+
import type { PgConfig } from 'pg-env';
|
|
13
|
+
import type { ServerOptions, CDNOptions, DeploymentOptions, MigrationOptions, JobsConfig, PgTestConnectionOptions } from '@pgpmjs/types';
|
|
14
|
+
import type { ConstructiveOptions, GraphileOptions, GraphileFeatureOptions, ApiOptions } from '@constructive-io/graphql-types';
|
|
15
|
+
export type { PgConfig, ServerOptions, CDNOptions, DeploymentOptions, MigrationOptions, JobsConfig, PgTestConnectionOptions, GraphileOptions, GraphileFeatureOptions, ApiOptions, ConstructiveOptions };
|
|
16
|
+
export type CdnOptions = CDNOptions;
|
|
17
|
+
export type JobsOptions = JobsConfig;
|
|
18
|
+
export type DbOptions = PgTestConnectionOptions;
|
|
19
|
+
/**
|
|
20
|
+
* Default configuration values for GraphQL server
|
|
21
|
+
*
|
|
22
|
+
* Provides sensible defaults for all currently active fields.
|
|
23
|
+
*/
|
|
24
|
+
export declare const serverDefaults: Partial<ConstructiveOptions>;
|
|
25
|
+
/**
|
|
26
|
+
* Type guard to validate if an unknown value is a valid ConstructiveOptions object
|
|
27
|
+
*
|
|
28
|
+
* Validates that:
|
|
29
|
+
* 1. The value is a non-null object
|
|
30
|
+
* 2. Contains at least one recognized field from the interface
|
|
31
|
+
* 3. All recognized fields that exist have object values (not primitives)
|
|
32
|
+
*
|
|
33
|
+
* @param opts - Unknown value to validate
|
|
34
|
+
* @returns True if opts is a valid ConstructiveOptions object
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* if (isConstructiveOptions(unknownConfig)) {
|
|
39
|
+
* // TypeScript knows unknownConfig is ConstructiveOptions
|
|
40
|
+
* const { pg, server } = unknownConfig;
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function isConstructiveOptions(opts: unknown): opts is ConstructiveOptions;
|
|
45
|
+
/**
|
|
46
|
+
* Type guard to check if an object has PostgreSQL configuration
|
|
47
|
+
*
|
|
48
|
+
* @param opts - Unknown value to check
|
|
49
|
+
* @returns True if opts has a defined pg property
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* if (hasPgConfig(config)) {
|
|
54
|
+
* console.log(config.pg.host);
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function hasPgConfig(opts: unknown): opts is {
|
|
59
|
+
pg: Partial<PgConfig>;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Type guard to check if an object has HTTP server configuration
|
|
63
|
+
*
|
|
64
|
+
* @param opts - Unknown value to check
|
|
65
|
+
* @returns True if opts has a defined server property
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* if (hasServerConfig(config)) {
|
|
70
|
+
* console.log(config.server.port);
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function hasServerConfig(opts: unknown): opts is {
|
|
75
|
+
server: ServerOptions;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Type guard to check if an object has API configuration
|
|
79
|
+
*
|
|
80
|
+
* @param opts - Unknown value to check
|
|
81
|
+
* @returns True if opts has a defined api property
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* if (hasApiConfig(config)) {
|
|
86
|
+
* console.log(config.api.exposedSchemas);
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export declare function hasApiConfig(opts: unknown): opts is {
|
|
91
|
+
api: ApiOptions;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Detects if the given options object uses a deprecated/legacy format
|
|
95
|
+
*
|
|
96
|
+
* Checks for presence of legacy field names that indicate the configuration
|
|
97
|
+
* needs to be migrated to ConstructiveOptions format.
|
|
98
|
+
*
|
|
99
|
+
* @param opts - Unknown value to check
|
|
100
|
+
* @returns True if legacy configuration patterns are detected
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* if (isLegacyOptions(config)) {
|
|
105
|
+
* console.warn('Detected legacy configuration format. Please migrate to ConstructiveOptions.');
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export declare function isLegacyOptions(opts: unknown): boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Normalizes input to a ConstructiveOptions object with defaults applied
|
|
112
|
+
*
|
|
113
|
+
* Accepts ConstructiveOptions and returns a fully normalized object
|
|
114
|
+
* with default values applied via deep merge. User-provided values override defaults.
|
|
115
|
+
*
|
|
116
|
+
* @param opts - ConstructiveOptions to normalize
|
|
117
|
+
* @returns ConstructiveOptions with defaults filled in
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* // Partial config - missing fields filled from defaults
|
|
122
|
+
* const normalized = normalizeServerOptions({
|
|
123
|
+
* pg: { database: 'myapp' }
|
|
124
|
+
* });
|
|
125
|
+
*
|
|
126
|
+
* // normalized.pg.host === 'localhost' (from default)
|
|
127
|
+
* // normalized.pg.database === 'myapp' (from user config)
|
|
128
|
+
* // normalized.server.port === 3000 (from default)
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export declare function normalizeServerOptions(opts: ConstructiveOptions): ConstructiveOptions;
|