@constructive-io/graphql-server 3.1.1 → 4.0.1
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/esm/middleware/auth.js
CHANGED
|
@@ -8,6 +8,7 @@ const isDev = () => getNodeEnv() === 'development';
|
|
|
8
8
|
export const createAuthenticateMiddleware = (opts) => {
|
|
9
9
|
return async (req, res, next) => {
|
|
10
10
|
const api = req.api;
|
|
11
|
+
log.info(`[auth] middleware called, api=${api ? 'present' : 'missing'}`);
|
|
11
12
|
if (!api) {
|
|
12
13
|
res.status(500).send('Missing API info');
|
|
13
14
|
return;
|
|
@@ -17,19 +18,26 @@ export const createAuthenticateMiddleware = (opts) => {
|
|
|
17
18
|
database: api.dbname,
|
|
18
19
|
});
|
|
19
20
|
const rlsModule = api.rlsModule;
|
|
21
|
+
log.info(`[auth] rlsModule=${rlsModule ? 'present' : 'missing'}, ` +
|
|
22
|
+
`authenticate=${rlsModule?.authenticate ?? 'none'}, ` +
|
|
23
|
+
`authenticateStrict=${rlsModule?.authenticateStrict ?? 'none'}, ` +
|
|
24
|
+
`privateSchema=${rlsModule?.privateSchema?.schemaName ?? 'none'}`);
|
|
20
25
|
if (!rlsModule) {
|
|
21
|
-
|
|
22
|
-
log.debug('No RLS module configured, skipping auth');
|
|
26
|
+
log.info('[auth] No RLS module configured, skipping auth');
|
|
23
27
|
return next();
|
|
24
28
|
}
|
|
25
|
-
const authFn = opts.server
|
|
29
|
+
const authFn = opts.server?.strictAuth
|
|
26
30
|
? rlsModule.authenticateStrict
|
|
27
31
|
: rlsModule.authenticate;
|
|
32
|
+
log.info(`[auth] strictAuth=${opts.server?.strictAuth ?? false}, authFn=${authFn ?? 'none'}`);
|
|
28
33
|
if (authFn && rlsModule.privateSchema.schemaName) {
|
|
29
34
|
const { authorization = '' } = req.headers;
|
|
30
35
|
const [authType, authToken] = authorization.split(' ');
|
|
31
36
|
let token = {};
|
|
37
|
+
log.info(`[auth] authorization header present=${!!authorization}, ` +
|
|
38
|
+
`authType=${authType ?? 'none'}, hasToken=${!!authToken}`);
|
|
32
39
|
if (authType?.toLowerCase() === 'bearer' && authToken) {
|
|
40
|
+
log.info('[auth] Processing bearer token authentication');
|
|
33
41
|
const context = {
|
|
34
42
|
'jwt.claims.ip_address': req.clientIp,
|
|
35
43
|
};
|
|
@@ -39,25 +47,28 @@ export const createAuthenticateMiddleware = (opts) => {
|
|
|
39
47
|
if (req.get('User-Agent')) {
|
|
40
48
|
context['jwt.claims.user_agent'] = req.get('User-Agent');
|
|
41
49
|
}
|
|
50
|
+
const authQuery = `SELECT * FROM "${rlsModule.privateSchema.schemaName}"."${authFn}"($1)`;
|
|
51
|
+
log.info(`[auth] Executing auth query: ${authQuery}`);
|
|
42
52
|
try {
|
|
43
53
|
const result = await pgQueryContext({
|
|
44
54
|
client: pool,
|
|
45
55
|
context,
|
|
46
|
-
query:
|
|
56
|
+
query: authQuery,
|
|
47
57
|
variables: [authToken],
|
|
48
58
|
});
|
|
59
|
+
log.info(`[auth] Query result: rowCount=${result?.rowCount}`);
|
|
49
60
|
if (result?.rowCount === 0) {
|
|
61
|
+
log.info('[auth] No rows returned, returning UNAUTHENTICATED');
|
|
50
62
|
res.status(200).json({
|
|
51
63
|
errors: [{ extensions: { code: 'UNAUTHENTICATED' } }],
|
|
52
64
|
});
|
|
53
65
|
return;
|
|
54
66
|
}
|
|
55
67
|
token = result.rows[0];
|
|
56
|
-
|
|
57
|
-
log.debug(`Auth success: role=${token.role}`);
|
|
68
|
+
log.info(`[auth] Auth success: role=${token.role}, user_id=${token.user_id}`);
|
|
58
69
|
}
|
|
59
70
|
catch (e) {
|
|
60
|
-
log.error('Auth error:', e.message);
|
|
71
|
+
log.error('[auth] Auth error:', e.message);
|
|
61
72
|
res.status(200).json({
|
|
62
73
|
errors: [
|
|
63
74
|
{
|
|
@@ -71,8 +82,15 @@ export const createAuthenticateMiddleware = (opts) => {
|
|
|
71
82
|
return;
|
|
72
83
|
}
|
|
73
84
|
}
|
|
85
|
+
else {
|
|
86
|
+
log.info('[auth] No bearer token provided, using anonymous auth');
|
|
87
|
+
}
|
|
74
88
|
req.token = token;
|
|
75
89
|
}
|
|
90
|
+
else {
|
|
91
|
+
log.info(`[auth] Skipping auth: authFn=${authFn ?? 'none'}, ` +
|
|
92
|
+
`privateSchema=${rlsModule.privateSchema?.schemaName ?? 'none'}`);
|
|
93
|
+
}
|
|
76
94
|
next();
|
|
77
95
|
};
|
|
78
96
|
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getNodeEnv } from '@constructive-io/graphql-env';
|
|
2
|
+
import { Logger } from '@pgpmjs/logger';
|
|
3
|
+
import { isApiError } from '../errors/api-errors';
|
|
4
|
+
import errorPage404Message from '../errors/404-message';
|
|
5
|
+
import errorPage50x from '../errors/50x';
|
|
6
|
+
import './types';
|
|
7
|
+
const log = new Logger('error-handler');
|
|
8
|
+
const isDevelopment = () => getNodeEnv() === 'development';
|
|
9
|
+
const wantsJson = (req) => {
|
|
10
|
+
const accept = req.get('Accept') || '';
|
|
11
|
+
return accept.includes('application/json') || accept.includes('application/graphql-response+json');
|
|
12
|
+
};
|
|
13
|
+
const sanitizeMessage = (error) => {
|
|
14
|
+
if (isDevelopment())
|
|
15
|
+
return error.message;
|
|
16
|
+
if (isApiError(error))
|
|
17
|
+
return error.message;
|
|
18
|
+
if (error.message?.includes('ECONNREFUSED'))
|
|
19
|
+
return 'Service temporarily unavailable';
|
|
20
|
+
if (error.message?.includes('timeout') || error.message?.includes('ETIMEDOUT'))
|
|
21
|
+
return 'Request timed out';
|
|
22
|
+
if (error.message?.includes('does not exist'))
|
|
23
|
+
return 'The requested resource does not exist';
|
|
24
|
+
return 'An unexpected error occurred';
|
|
25
|
+
};
|
|
26
|
+
const categorizeError = (err) => {
|
|
27
|
+
if (isApiError(err)) {
|
|
28
|
+
return {
|
|
29
|
+
statusCode: err.statusCode,
|
|
30
|
+
code: err.code,
|
|
31
|
+
message: sanitizeMessage(err),
|
|
32
|
+
logLevel: err.statusCode >= 500 ? 'error' : 'warn',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (err.message?.includes('ECONNREFUSED') || err.message?.includes('connection terminated')) {
|
|
36
|
+
return { statusCode: 503, code: 'SERVICE_UNAVAILABLE', message: sanitizeMessage(err), logLevel: 'error' };
|
|
37
|
+
}
|
|
38
|
+
if (err.message?.includes('timeout') || err.message?.includes('ETIMEDOUT')) {
|
|
39
|
+
return { statusCode: 504, code: 'GATEWAY_TIMEOUT', message: sanitizeMessage(err), logLevel: 'error' };
|
|
40
|
+
}
|
|
41
|
+
return { statusCode: 500, code: 'INTERNAL_ERROR', message: sanitizeMessage(err), logLevel: 'error' };
|
|
42
|
+
};
|
|
43
|
+
const sendResponse = (req, res, { statusCode, code, message }) => {
|
|
44
|
+
if (wantsJson(req)) {
|
|
45
|
+
res.status(statusCode).json({ error: { code, message, requestId: req.requestId } });
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
res.status(statusCode).send(statusCode >= 500 ? errorPage50x : errorPage404Message(message));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const logError = (err, req, level) => {
|
|
52
|
+
const context = {
|
|
53
|
+
requestId: req.requestId,
|
|
54
|
+
path: req.path,
|
|
55
|
+
method: req.method,
|
|
56
|
+
host: req.get('host'),
|
|
57
|
+
databaseId: req.databaseId,
|
|
58
|
+
svcKey: req.svc_key,
|
|
59
|
+
clientIp: req.clientIp,
|
|
60
|
+
};
|
|
61
|
+
if (isApiError(err)) {
|
|
62
|
+
log[level]({ event: 'api_error', code: err.code, statusCode: err.statusCode, message: err.message, ...context });
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
log[level]({ event: 'unexpected_error', name: err.name, message: err.message, stack: isDevelopment() ? err.stack : undefined, ...context });
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
export const errorHandler = (err, req, res, _next) => {
|
|
69
|
+
if (res.headersSent) {
|
|
70
|
+
log.warn({ event: 'headers_already_sent', requestId: req.requestId, path: req.path, errorMessage: err.message });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const response = categorizeError(err);
|
|
74
|
+
logError(err, req, response.logLevel);
|
|
75
|
+
sendResponse(req, res, response);
|
|
76
|
+
};
|
|
77
|
+
export const notFoundHandler = (req, res, _next) => {
|
|
78
|
+
const message = `Route not found: ${req.method} ${req.path}`;
|
|
79
|
+
log.warn({ event: 'route_not_found', path: req.path, method: req.method, requestId: req.requestId });
|
|
80
|
+
if (wantsJson(req)) {
|
|
81
|
+
res.status(404).json({ error: { code: 'NOT_FOUND', message, requestId: req.requestId } });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
res.status(404).send(errorPage404Message(message));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const FAVICON_BASE64 = 'AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAABMLAAATCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+gAQD/nwAJ/6ECOv+iAXL/oQEm/50AAv+eAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+fAAD/nQAC/6EBLv+hAVv/ogJy/6EB2f+hAdH/oAFr/6AAGP+hAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+hAgD/ogMB/6ABF/+iAVL/oQFY/6MCJP+hAi3/oQHU/6EB//+hAff/oQG2/6EBT/+gAAr/oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/nwAA/58AC/+hAUX/oQGh/6ABtf+hAC//oQIA/6ECK/+hAdT/oQH//6EB//+hAf//oQHv/6EBlf+gATT/nQAC/58AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/oQIA/6ECBv+hASv/ogFi/6ECSP+hAon/oQH+/6EBy/+hAWf/oQI9/6EB1f+hAf//oQH//6EB//+hAf//oQH//6EB2f+hAXH/oAAb/7gYAP+iAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/6EBAP+hAQj/oQFT/6ICXf+iAiL/nwkE/6ACfP+hAf//oQH//6EB9P+hAc3/oQHr/6EB//+hAf//oQH//6EB//+hAf//oQH//6EB+f+iAsH/oQFD/6ICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/ogIA/6ICFP+iAmT/ngAD/58CAP+XDwL/oAJ8/6EB//+hAf//oQH//6EB//+iAcj/ogHO/6EB+v+hAf//oQH//6EB//+hAf//oQH//6EB//+hAaX/oQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+iAgD/ogIS/6ICWP+iAgD/nwQA/5cQAf+gAnz/oQH//6EB//+hAf//oQH//6ECj/+iAyL/oQF6/6EB3f+hAf7/oQH//6EB//+hAf//oQH//6EBpv+hAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/6ICAP+iAhP/ogJZ/6ECAP+dAAL/oQEi/6EBov+hAf//oQH//6EB//+hAf//oQKO/6MFBf+lAwT/ogFO/6EBwv+hAfb/oQH//6EB//+hAf//oQGm/6EBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/ogIA/6ICE/+iAln/oQAL/6EBRv+hAWP/oQGd/6EB//+hAf//oQH//6EB//+hAo3/oAIQ/6EBRf+hAl//oQI5/6ECV/+hAcP/oQH7/6EB//+hAab/oQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+hAQD/oQET/6EBiP+iAmL/oQFF/6EFDf+gAnv/oQH//6EB//+hAf//oQH//6EBsP+iAWX/ogFE/6EADP+iAQD/ogEA/6ICH/+iAXT/oQHk/6EBp/+hAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/6EBAP+hARb/oQF8/6EBFv+lAgH/lxAB/6ACfP+hAf//oQH//6EB//+hAf//oQHT/6EBY/+gARL/oAEAAAAAAP+dAAD/pQQA/6ACG/+iAn//oQFp/6ICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/ogIA/6ICE/+iAln/ogIA/58EAP+WEQH/oAJ7/6EB//+hAf//oQH//6EB//+hApz/ogFJ/6IBYv+hAS7/ogEH/6ABC/+gAED/oQJb/6ICM/+hAQj/ogIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+iAgD/ogIT/6ICWf+iAgD/oAMA/58CDP+hApH/oQH//6EB//+hAf//oQH//6ECjv+kBQf/ogIa/6ICVf+hAVz/oQFk/6ICTv+hAhD/oQIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/6ICAP+iAhL/ogJY/6ICAf+gACb/oQFk/6ICdP+iAa//oQH0/6EB//+hAf//oQKO/6MFBf+hAgD/nwAD/6IBLP+jAiD/ogEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/ogIA/6ICE/+iAnP/oQJL/6EBcP+iAin/owQF/6MCD/+hAWL/oQHE/6EB/P+hAo7/owUF/6IDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+hAQD/oQEU/6EBp/+iAl//oAAJ/6IBAAAAAAAAAAAA/6EBAP+iAij/oQGn/6EBrP+hAR//mwAC/50AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/6ABAP+dAAL/oQIy/6EBYP+hAT//nwAN/58AAP+cAAL/oQEn/6EBVv+iAXv/oQHY/6EBzP+gAV7/oAAQ/6EBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+iAgD/owMR/6ICRv+hAWj/oAE1/6ECS/+hAWX/oQIu/6ECLf+hAdT/oQH//6EB8v+hAav/oQFB/54AB/+gAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+jBAD/pAUD/6ACHv+hAXL/oQFN/6ECCf+hAgD/oQIr/6EB1P+hAf//oQH//6EB//+hAef/oAGJ/6ABJf+ZAAH/nwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+fAAD/lgAB/6ABP/+hAi3/oQIA/6ECAP+hAiv/oQHU/6EB//+hAf//oQH//6EB//+hAf3/oQHN/6IBaP+fARL/jwAA/54AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/58AAP+XAAH/oAFA/6ECLf+hAgD/ogMA/6ECOv+hAd3/oQH//6EB//+hAf//oQH//6EB//+hAf//oQH1/6ABsv+hAT7/oAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/nwAA/5gAAf+gAT//oQIt/6EABf+hATj/ogFm/6ICdf+hAdP/oQH8/6EB//+hAf//oQH//6EB//+hAf//oQH//6EBof+hAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+fAAD/kwAB/6ABQ/+hAlf/oQFY/6IBUP+iARH/rxEB/6ECJ/+hAY3/oQHj/6EB//+hAf//oQH//6EB//+hAf//oQGm/6EBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/58AAP+jAgD/oQFS/6EBjv+hAS//mwAC/50AAAAAAAD/oQIA/6ECA/+hAln/oQHT/6EB+f+hAf//oQH//6EB//+hAab/oQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/nwAA/6ABAP+gAQ//oQFS/6EBWf+hASb/mAAB/58AAP+fAAv/oQE8/6EBYf+hAj7/oQFr/6EBy/+hAf3/oQH//6EBpv+hAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/6MEAP+lCwH/ogIn/6ICW/+iAU//oQAu/6IBYP+iAU7/ogMZ/6EBAP+VAAD/ogIl/6IBiP+hAej/oQGm/6EBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+hAQD/ogIJ/6IBOv+hAYr/oQFf/6ABDf+gAQAAAAAA/54AAP+pAAD/oAAa/6EBe/+hAXP/oQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/ogIA/6MDEv+iAUv/ogFg/6EBKP+hAAT/ogEH/6EAOf+iAWn/oQE//6ABCf+iAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/pAIA/6QCBP+iAh3/ogJc/6EBUv+iAVn/ogFV/6ICFP+mBQL/pQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/6MEAP+jBAj/ogI2/6ICLv+fAAT/nwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////4P///4B///4AH//8EAf/8AAD/+AAAf/iAAH/5gAB/+QAAf/gAAH/4ABh/+AA8f/mAAH/5gAH/+ABH//gAf//4eD//+CAf//4AB///BAH//4wA//+MAH//gAB//4AAf//DgH//wQB//+AYf//4PH///gB///8A////w/8=';
|
|
2
|
+
const faviconBuffer = Buffer.from(FAVICON_BASE64, 'base64');
|
|
3
|
+
export const favicon = (req, res, next) => {
|
|
4
|
+
if (req.path === '/favicon.ico' && req.method === 'GET') {
|
|
5
|
+
res.setHeader('Content-Type', 'image/x-icon');
|
|
6
|
+
res.setHeader('Content-Length', faviconBuffer.length);
|
|
7
|
+
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
8
|
+
res.status(200).send(faviconBuffer);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
next();
|
|
12
|
+
};
|
|
@@ -1,12 +1,116 @@
|
|
|
1
1
|
import { Logger } from '@pgpmjs/logger';
|
|
2
|
-
import { graphileCache } from 'graphile-cache';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { createGraphileInstance, graphileCache } from 'graphile-cache';
|
|
3
|
+
import { ConstructivePreset, makePgService } from 'graphile-settings';
|
|
4
|
+
import { buildConnectionString } from 'pg-cache';
|
|
5
|
+
import { getPgEnvOptions } from 'pg-env';
|
|
6
6
|
import './types'; // for Request type
|
|
7
|
-
import
|
|
7
|
+
import { HandlerCreationError } from '../errors/api-errors';
|
|
8
|
+
/**
|
|
9
|
+
* Custom maskError function that always returns the original error.
|
|
10
|
+
*
|
|
11
|
+
* By default, grafserv masks errors for security (hiding sensitive database errors
|
|
12
|
+
* from clients). We disable this masking to show full error messages.
|
|
13
|
+
*
|
|
14
|
+
* Upstream reference:
|
|
15
|
+
* - grafserv defaultMaskError: node_modules/grafserv/dist/options.js
|
|
16
|
+
* - SafeError interface: grafast isSafeError() - errors implementing SafeError
|
|
17
|
+
* are shown as-is even with default masking
|
|
18
|
+
*
|
|
19
|
+
* If you need to restore masking behavior, see the upstream implementation which:
|
|
20
|
+
* 1. Returns GraphQLError instances as-is
|
|
21
|
+
* 2. Returns SafeError instances with their message exposed
|
|
22
|
+
* 3. Masks other errors with a hash/ID and logs the original
|
|
23
|
+
*/
|
|
24
|
+
const maskError = (error) => {
|
|
25
|
+
return error;
|
|
26
|
+
};
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Single-Flight Pattern: In-Flight Tracking
|
|
29
|
+
// =============================================================================
|
|
30
|
+
/**
|
|
31
|
+
* Tracks in-flight handler creation promises to prevent duplicate creations.
|
|
32
|
+
* When multiple concurrent requests arrive for the same cache key, only the
|
|
33
|
+
* first request creates the handler while others wait on the same promise.
|
|
34
|
+
*/
|
|
35
|
+
const creating = new Map();
|
|
36
|
+
/**
|
|
37
|
+
* Returns the number of currently in-flight handler creation operations.
|
|
38
|
+
* Useful for monitoring and debugging.
|
|
39
|
+
*/
|
|
40
|
+
export function getInFlightCount() {
|
|
41
|
+
return creating.size;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Returns the cache keys for all currently in-flight handler creation operations.
|
|
45
|
+
* Useful for monitoring and debugging.
|
|
46
|
+
*/
|
|
47
|
+
export function getInFlightKeys() {
|
|
48
|
+
return [...creating.keys()];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Clears the in-flight map. Used for testing purposes.
|
|
52
|
+
*/
|
|
53
|
+
export function clearInFlightMap() {
|
|
54
|
+
creating.clear();
|
|
55
|
+
}
|
|
8
56
|
const log = new Logger('graphile');
|
|
9
57
|
const reqLabel = (req) => req.requestId ? `[${req.requestId}]` : '[req]';
|
|
58
|
+
/**
|
|
59
|
+
* Build a PostGraphile v5 preset for a tenant
|
|
60
|
+
*/
|
|
61
|
+
const buildPreset = (connectionString, schemas, anonRole, roleName) => ({
|
|
62
|
+
extends: [ConstructivePreset],
|
|
63
|
+
pgServices: [
|
|
64
|
+
makePgService({
|
|
65
|
+
connectionString,
|
|
66
|
+
schemas,
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
69
|
+
grafserv: {
|
|
70
|
+
graphqlPath: '/graphql',
|
|
71
|
+
graphiqlPath: '/graphiql',
|
|
72
|
+
graphiql: true,
|
|
73
|
+
maskError,
|
|
74
|
+
},
|
|
75
|
+
grafast: {
|
|
76
|
+
explain: process.env.NODE_ENV === 'development',
|
|
77
|
+
context: (requestContext) => {
|
|
78
|
+
// In grafserv/express/v4, the request is available at requestContext.expressv4.req
|
|
79
|
+
const req = requestContext?.expressv4?.req;
|
|
80
|
+
const context = {};
|
|
81
|
+
if (req) {
|
|
82
|
+
if (req.databaseId) {
|
|
83
|
+
context['jwt.claims.database_id'] = req.databaseId;
|
|
84
|
+
}
|
|
85
|
+
if (req.clientIp) {
|
|
86
|
+
context['jwt.claims.ip_address'] = req.clientIp;
|
|
87
|
+
}
|
|
88
|
+
if (req.get('origin')) {
|
|
89
|
+
context['jwt.claims.origin'] = req.get('origin');
|
|
90
|
+
}
|
|
91
|
+
if (req.get('User-Agent')) {
|
|
92
|
+
context['jwt.claims.user_agent'] = req.get('User-Agent');
|
|
93
|
+
}
|
|
94
|
+
if (req.token?.user_id) {
|
|
95
|
+
return {
|
|
96
|
+
pgSettings: {
|
|
97
|
+
role: roleName,
|
|
98
|
+
'jwt.claims.token_id': req.token.id,
|
|
99
|
+
'jwt.claims.user_id': req.token.user_id,
|
|
100
|
+
...context,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
pgSettings: {
|
|
107
|
+
role: anonRole,
|
|
108
|
+
...context,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
10
114
|
export const graphile = (opts) => {
|
|
11
115
|
return async (req, res, next) => {
|
|
12
116
|
const label = reqLabel(req);
|
|
@@ -23,76 +127,57 @@ export const graphile = (opts) => {
|
|
|
23
127
|
}
|
|
24
128
|
const { dbname, anonRole, roleName, schema } = api;
|
|
25
129
|
const schemaLabel = schema?.join(',') || 'unknown';
|
|
130
|
+
// =========================================================================
|
|
131
|
+
// Phase A: Cache Check (fast path)
|
|
132
|
+
// =========================================================================
|
|
26
133
|
const cached = graphileCache.get(key);
|
|
27
134
|
if (cached) {
|
|
28
135
|
log.debug(`${label} PostGraphile cache hit key=${key} db=${dbname} schemas=${schemaLabel}`);
|
|
29
136
|
return cached.handler(req, res, next);
|
|
30
137
|
}
|
|
31
138
|
log.debug(`${label} PostGraphile cache miss key=${key} db=${dbname} schemas=${schemaLabel}`);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
log.info(`${label} Enabling PublicKeySignature plugin for ${dbname}`);
|
|
42
|
-
options.appendPlugins.push(PublicKeySignature(pubkey_challenge.data));
|
|
43
|
-
}
|
|
44
|
-
options.appendPlugins = options.appendPlugins ?? [];
|
|
45
|
-
if (opts.graphile?.appendPlugins) {
|
|
46
|
-
options.appendPlugins.push(...opts.graphile.appendPlugins);
|
|
47
|
-
}
|
|
48
|
-
options.pgSettings = async function pgSettings(request) {
|
|
49
|
-
const gqlReq = request;
|
|
50
|
-
const settingsLabel = reqLabel(gqlReq);
|
|
51
|
-
const context = {
|
|
52
|
-
[`jwt.claims.database_id`]: gqlReq.databaseId,
|
|
53
|
-
[`jwt.claims.ip_address`]: gqlReq.clientIp,
|
|
54
|
-
};
|
|
55
|
-
if (gqlReq.get('origin')) {
|
|
56
|
-
context['jwt.claims.origin'] = gqlReq.get('origin');
|
|
139
|
+
// =========================================================================
|
|
140
|
+
// Phase B: In-Flight Check (single-flight coalescing)
|
|
141
|
+
// =========================================================================
|
|
142
|
+
const inFlight = creating.get(key);
|
|
143
|
+
if (inFlight) {
|
|
144
|
+
log.debug(`${label} Coalescing request for PostGraphile[${key}] - waiting for in-flight creation`);
|
|
145
|
+
try {
|
|
146
|
+
const instance = await inFlight;
|
|
147
|
+
return instance.handler(req, res, next);
|
|
57
148
|
}
|
|
58
|
-
|
|
59
|
-
|
|
149
|
+
catch (error) {
|
|
150
|
+
// Re-throw to be caught by outer try-catch
|
|
151
|
+
throw error;
|
|
60
152
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
...context,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
log.debug(`${settingsLabel} pgSettings role=${anonRole} db=${gqlReq.databaseId} ip=${gqlReq.clientIp}`);
|
|
71
|
-
return { role: anonRole, ...context };
|
|
72
|
-
};
|
|
73
|
-
options.graphqlRoute = '/graphql';
|
|
74
|
-
options.graphiqlRoute = '/graphiql';
|
|
75
|
-
options.graphileBuildOptions = {
|
|
76
|
-
...options.graphileBuildOptions,
|
|
77
|
-
...opts.graphile?.graphileBuildOptions,
|
|
78
|
-
};
|
|
79
|
-
const graphileOpts = {
|
|
80
|
-
...options,
|
|
81
|
-
...opts.graphile?.overrideSettings,
|
|
82
|
-
};
|
|
83
|
-
log.info(`${label} Building PostGraphile handler key=${key} db=${dbname} schemas=${schemaLabel} role=${roleName} anon=${anonRole}`);
|
|
84
|
-
const pgPool = getPgPool({
|
|
153
|
+
}
|
|
154
|
+
// =========================================================================
|
|
155
|
+
// Phase C: Create New Handler (first request for this key)
|
|
156
|
+
// =========================================================================
|
|
157
|
+
log.info(`${label} Building PostGraphile v5 handler key=${key} db=${dbname} schemas=${schemaLabel} role=${roleName} anon=${anonRole}`);
|
|
158
|
+
const pgConfig = getPgEnvOptions({
|
|
85
159
|
...opts.pg,
|
|
86
160
|
database: dbname,
|
|
87
161
|
});
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
162
|
+
const connectionString = buildConnectionString(pgConfig.user, pgConfig.password, pgConfig.host, pgConfig.port, pgConfig.database);
|
|
163
|
+
// Create promise and store in in-flight map BEFORE try block
|
|
164
|
+
const preset = buildPreset(connectionString, schema || [], anonRole, roleName);
|
|
165
|
+
const creationPromise = createGraphileInstance({ preset, cacheKey: key });
|
|
166
|
+
creating.set(key, creationPromise);
|
|
167
|
+
try {
|
|
168
|
+
const instance = await creationPromise;
|
|
169
|
+
graphileCache.set(key, instance);
|
|
170
|
+
log.info(`${label} Cached PostGraphile v5 handler key=${key} db=${dbname}`);
|
|
171
|
+
return instance.handler(req, res, next);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
log.error(`${label} Failed to create PostGraphile[${key}]:`, error);
|
|
175
|
+
throw new HandlerCreationError(`Failed to create handler for ${key}: ${error instanceof Error ? error.message : String(error)}`, { cacheKey: key, cause: error instanceof Error ? error.message : String(error) });
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
// Always clean up in-flight tracker
|
|
179
|
+
creating.delete(key);
|
|
180
|
+
}
|
|
96
181
|
}
|
|
97
182
|
catch (e) {
|
|
98
183
|
log.error(`${label} PostGraphile middleware error`, e);
|
package/esm/options.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
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 deepmerge from 'deepmerge';
|
|
13
|
+
import { graphileDefaults, graphileFeatureDefaults, apiDefaults } from '@constructive-io/graphql-types';
|
|
14
|
+
// ============================================
|
|
15
|
+
// Default Configuration
|
|
16
|
+
// ============================================
|
|
17
|
+
/**
|
|
18
|
+
* Default configuration values for GraphQL server
|
|
19
|
+
*
|
|
20
|
+
* Provides sensible defaults for all currently active fields.
|
|
21
|
+
*/
|
|
22
|
+
export const serverDefaults = {
|
|
23
|
+
pg: {
|
|
24
|
+
host: 'localhost',
|
|
25
|
+
port: 5432,
|
|
26
|
+
user: 'postgres',
|
|
27
|
+
password: 'password',
|
|
28
|
+
database: 'postgres'
|
|
29
|
+
},
|
|
30
|
+
server: {
|
|
31
|
+
host: 'localhost',
|
|
32
|
+
port: 3000,
|
|
33
|
+
trustProxy: false,
|
|
34
|
+
strictAuth: false
|
|
35
|
+
},
|
|
36
|
+
api: apiDefaults,
|
|
37
|
+
graphile: graphileDefaults,
|
|
38
|
+
features: graphileFeatureDefaults
|
|
39
|
+
};
|
|
40
|
+
// ============================================
|
|
41
|
+
// Type Guards
|
|
42
|
+
// ============================================
|
|
43
|
+
/**
|
|
44
|
+
* List of all recognized fields in ConstructiveOptions
|
|
45
|
+
*/
|
|
46
|
+
const RECOGNIZED_FIELDS = [
|
|
47
|
+
'pg',
|
|
48
|
+
'server',
|
|
49
|
+
'api',
|
|
50
|
+
'graphile',
|
|
51
|
+
'features',
|
|
52
|
+
'db',
|
|
53
|
+
'cdn',
|
|
54
|
+
'deployment',
|
|
55
|
+
'migrations',
|
|
56
|
+
'jobs'
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Type guard to validate if an unknown value is a valid ConstructiveOptions object
|
|
60
|
+
*
|
|
61
|
+
* Validates that:
|
|
62
|
+
* 1. The value is a non-null object
|
|
63
|
+
* 2. Contains at least one recognized field from the interface
|
|
64
|
+
* 3. All recognized fields that exist have object values (not primitives)
|
|
65
|
+
*
|
|
66
|
+
* @param opts - Unknown value to validate
|
|
67
|
+
* @returns True if opts is a valid ConstructiveOptions object
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* if (isConstructiveOptions(unknownConfig)) {
|
|
72
|
+
* // TypeScript knows unknownConfig is ConstructiveOptions
|
|
73
|
+
* const { pg, server } = unknownConfig;
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function isConstructiveOptions(opts) {
|
|
78
|
+
if (opts === null || opts === undefined) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (typeof opts !== 'object') {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const obj = opts;
|
|
85
|
+
// Check for at least one recognized field from the interface
|
|
86
|
+
const hasRecognizedField = RECOGNIZED_FIELDS.some((field) => field in obj);
|
|
87
|
+
if (!hasRecognizedField) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
// Validate that recognized fields have object values (not primitives)
|
|
91
|
+
for (const field of RECOGNIZED_FIELDS) {
|
|
92
|
+
if (field in obj && obj[field] !== undefined && obj[field] !== null) {
|
|
93
|
+
if (typeof obj[field] !== 'object') {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Type guard to check if an object has PostgreSQL configuration
|
|
102
|
+
*
|
|
103
|
+
* @param opts - Unknown value to check
|
|
104
|
+
* @returns True if opts has a defined pg property
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* if (hasPgConfig(config)) {
|
|
109
|
+
* console.log(config.pg.host);
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function hasPgConfig(opts) {
|
|
114
|
+
if (opts === null || opts === undefined || typeof opts !== 'object') {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return 'pg' in opts && opts.pg !== undefined;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Type guard to check if an object has HTTP server configuration
|
|
121
|
+
*
|
|
122
|
+
* @param opts - Unknown value to check
|
|
123
|
+
* @returns True if opts has a defined server property
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* if (hasServerConfig(config)) {
|
|
128
|
+
* console.log(config.server.port);
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function hasServerConfig(opts) {
|
|
133
|
+
if (opts === null || opts === undefined || typeof opts !== 'object') {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return 'server' in opts && opts.server !== undefined;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Type guard to check if an object has API configuration
|
|
140
|
+
*
|
|
141
|
+
* @param opts - Unknown value to check
|
|
142
|
+
* @returns True if opts has a defined api property
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* if (hasApiConfig(config)) {
|
|
147
|
+
* console.log(config.api.exposedSchemas);
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export function hasApiConfig(opts) {
|
|
152
|
+
if (opts === null || opts === undefined || typeof opts !== 'object') {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return 'api' in opts && opts.api !== undefined;
|
|
156
|
+
}
|
|
157
|
+
// ============================================
|
|
158
|
+
// Internal Utilities
|
|
159
|
+
// ============================================
|
|
160
|
+
/**
|
|
161
|
+
* Array merge strategy that replaces arrays (source wins over target).
|
|
162
|
+
* This ensures that when a user specifies an array value, it replaces
|
|
163
|
+
* the default rather than merging/concatenating.
|
|
164
|
+
*
|
|
165
|
+
* @internal
|
|
166
|
+
*/
|
|
167
|
+
const replaceArrays = (_target, source) => source;
|
|
168
|
+
// ============================================
|
|
169
|
+
// Utility Functions
|
|
170
|
+
// ============================================
|
|
171
|
+
/**
|
|
172
|
+
* Legacy field names that indicate old configuration format
|
|
173
|
+
*/
|
|
174
|
+
const LEGACY_FIELDS = [
|
|
175
|
+
'schemas', // Old array-style schema config (should be graphile.schema)
|
|
176
|
+
'pgConfig', // Old naming (should be pg)
|
|
177
|
+
'serverPort', // Flat config (should be server.port)
|
|
178
|
+
'serverHost', // Flat config (should be server.host)
|
|
179
|
+
'dbConfig', // Old naming (should be db)
|
|
180
|
+
'postgraphile', // Old Graphile v4 naming (should be graphile)
|
|
181
|
+
'pgPool', // Direct pool config (deprecated)
|
|
182
|
+
'jwtSecret', // Flat JWT config (should be in api or auth)
|
|
183
|
+
'watchPg' // Old PostGraphile v4 option
|
|
184
|
+
];
|
|
185
|
+
/**
|
|
186
|
+
* Detects if the given options object uses a deprecated/legacy format
|
|
187
|
+
*
|
|
188
|
+
* Checks for presence of legacy field names that indicate the configuration
|
|
189
|
+
* needs to be migrated to ConstructiveOptions format.
|
|
190
|
+
*
|
|
191
|
+
* @param opts - Unknown value to check
|
|
192
|
+
* @returns True if legacy configuration patterns are detected
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* if (isLegacyOptions(config)) {
|
|
197
|
+
* console.warn('Detected legacy configuration format. Please migrate to ConstructiveOptions.');
|
|
198
|
+
* }
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export function isLegacyOptions(opts) {
|
|
202
|
+
if (opts === null || opts === undefined || typeof opts !== 'object') {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
const obj = opts;
|
|
206
|
+
return LEGACY_FIELDS.some((field) => field in obj);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Normalizes input to a ConstructiveOptions object with defaults applied
|
|
210
|
+
*
|
|
211
|
+
* Accepts ConstructiveOptions and returns a fully normalized object
|
|
212
|
+
* with default values applied via deep merge. User-provided values override defaults.
|
|
213
|
+
*
|
|
214
|
+
* @param opts - ConstructiveOptions to normalize
|
|
215
|
+
* @returns ConstructiveOptions with defaults filled in
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```typescript
|
|
219
|
+
* // Partial config - missing fields filled from defaults
|
|
220
|
+
* const normalized = normalizeServerOptions({
|
|
221
|
+
* pg: { database: 'myapp' }
|
|
222
|
+
* });
|
|
223
|
+
*
|
|
224
|
+
* // normalized.pg.host === 'localhost' (from default)
|
|
225
|
+
* // normalized.pg.database === 'myapp' (from user config)
|
|
226
|
+
* // normalized.server.port === 3000 (from default)
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
export function normalizeServerOptions(opts) {
|
|
230
|
+
// Deep merge with defaults - user options override defaults
|
|
231
|
+
return deepmerge(serverDefaults, opts, { arrayMerge: replaceArrays });
|
|
232
|
+
}
|