@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.
Files changed (77) hide show
  1. package/errors/404-message.js +1 -1
  2. package/errors/api-errors.d.ts +200 -0
  3. package/errors/api-errors.js +276 -0
  4. package/esm/errors/404-message.js +1 -1
  5. package/esm/errors/api-errors.js +261 -0
  6. package/esm/index.js +2 -0
  7. package/esm/middleware/api.js +355 -277
  8. package/esm/middleware/auth.js +25 -7
  9. package/esm/middleware/error-handler.js +86 -0
  10. package/esm/middleware/favicon.js +12 -0
  11. package/esm/middleware/graphile.js +149 -64
  12. package/esm/options.js +232 -0
  13. package/esm/schema.js +24 -11
  14. package/esm/server.js +41 -5
  15. package/index.d.ts +1 -0
  16. package/index.js +2 -0
  17. package/middleware/api.d.ts +3 -15
  18. package/middleware/api.js +359 -283
  19. package/middleware/auth.js +25 -7
  20. package/middleware/error-handler.d.ts +4 -0
  21. package/middleware/error-handler.js +94 -0
  22. package/middleware/favicon.d.ts +2 -0
  23. package/middleware/favicon.js +16 -0
  24. package/middleware/graphile.d.ts +14 -0
  25. package/middleware/graphile.js +149 -64
  26. package/options.d.ts +131 -0
  27. package/options.js +244 -0
  28. package/package.json +23 -24
  29. package/schema.d.ts +2 -2
  30. package/schema.js +23 -10
  31. package/server.d.ts +24 -2
  32. package/server.js +39 -3
  33. package/codegen/orm/client.d.ts +0 -55
  34. package/codegen/orm/client.js +0 -75
  35. package/codegen/orm/index.d.ts +0 -36
  36. package/codegen/orm/index.js +0 -59
  37. package/codegen/orm/input-types.d.ts +0 -20140
  38. package/codegen/orm/input-types.js +0 -2
  39. package/codegen/orm/models/api.d.ts +0 -42
  40. package/codegen/orm/models/api.js +0 -76
  41. package/codegen/orm/models/domain.d.ts +0 -42
  42. package/codegen/orm/models/domain.js +0 -76
  43. package/codegen/orm/models/index.d.ts +0 -7
  44. package/codegen/orm/models/index.js +0 -12
  45. package/codegen/orm/mutation/index.d.ts +0 -7
  46. package/codegen/orm/mutation/index.js +0 -7
  47. package/codegen/orm/query/index.d.ts +0 -20
  48. package/codegen/orm/query/index.js +0 -24
  49. package/codegen/orm/query-builder.d.ts +0 -81
  50. package/codegen/orm/query-builder.js +0 -496
  51. package/codegen/orm/select-types.d.ts +0 -83
  52. package/codegen/orm/select-types.js +0 -7
  53. package/codegen/orm/types.d.ts +0 -6
  54. package/codegen/orm/types.js +0 -23
  55. package/esm/codegen/orm/client.js +0 -70
  56. package/esm/codegen/orm/index.js +0 -39
  57. package/esm/codegen/orm/input-types.js +0 -1
  58. package/esm/codegen/orm/models/api.js +0 -72
  59. package/esm/codegen/orm/models/domain.js +0 -72
  60. package/esm/codegen/orm/models/index.js +0 -7
  61. package/esm/codegen/orm/mutation/index.js +0 -4
  62. package/esm/codegen/orm/query/index.js +0 -21
  63. package/esm/codegen/orm/query-builder.js +0 -452
  64. package/esm/codegen/orm/select-types.js +0 -6
  65. package/esm/codegen/orm/types.js +0 -7
  66. package/esm/middleware/gql.js +0 -116
  67. package/esm/plugins/PublicKeySignature.js +0 -114
  68. package/esm/scripts/codegen-schema.js +0 -71
  69. package/esm/scripts/create-bucket.js +0 -40
  70. package/middleware/gql.d.ts +0 -164
  71. package/middleware/gql.js +0 -121
  72. package/plugins/PublicKeySignature.d.ts +0 -11
  73. package/plugins/PublicKeySignature.js +0 -121
  74. package/scripts/codegen-schema.d.ts +0 -1
  75. package/scripts/codegen-schema.js +0 -76
  76. package/scripts/create-bucket.d.ts +0 -1
  77. package/scripts/create-bucket.js +0 -42
@@ -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
- if (isDev())
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.strictAuth
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: `SELECT * FROM "${rlsModule.privateSchema.schemaName}"."${authFn}"($1)`,
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
- if (isDev())
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,4 @@
1
+ import type { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
2
+ import './types';
3
+ export declare const errorHandler: ErrorRequestHandler;
4
+ export declare const notFoundHandler: (req: Request, res: Response, _next: NextFunction) => void;
@@ -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,2 @@
1
+ import type { RequestHandler } from 'express';
2
+ export declare const favicon: RequestHandler;
@@ -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;
@@ -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;
@@ -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 postgraphile_1 = require("postgraphile");
11
+ const pg_env_1 = require("pg-env");
12
12
  require("./types"); // for Request type
13
- const PublicKeySignature_1 = __importDefault(require("../plugins/PublicKeySignature"));
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
- const options = (0, graphile_settings_1.getGraphileSettings)({
39
- ...opts,
40
- graphile: {
41
- ...opts.graphile,
42
- schema: schema,
43
- },
44
- });
45
- const pubkey_challenge = api.apiModules.find((mod) => mod.name === 'pubkey_challenge');
46
- if (pubkey_challenge && pubkey_challenge.data) {
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
- if (gqlReq.get('User-Agent')) {
65
- context['jwt.claims.user_agent'] = gqlReq.get('User-Agent');
155
+ catch (error) {
156
+ // Re-throw to be caught by outer try-catch
157
+ throw error;
66
158
  }
67
- if (gqlReq?.token?.user_id) {
68
- log.debug(`${settingsLabel} pgSettings role=${roleName} db=${gqlReq.databaseId} ip=${gqlReq.clientIp}`);
69
- return {
70
- role: roleName,
71
- [`jwt.claims.token_id`]: gqlReq.token.id,
72
- [`jwt.claims.user_id`]: gqlReq.token.user_id,
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 handler = (0, postgraphile_1.postgraphile)(pgPool, schema, graphileOpts);
95
- graphile_cache_1.graphileCache.set(key, {
96
- pgPool,
97
- pgPoolKey: dbname,
98
- handler,
99
- });
100
- log.info(`${label} Cached PostGraphile handler key=${key} db=${dbname}`);
101
- return handler(req, res, next);
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;