@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.
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
package/middleware/api.js CHANGED
@@ -3,37 +3,93 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.getApiConfig = exports.getSvcKey = exports.queryServiceByApiName = exports.createApiMiddleware = exports.getSubdomain = exports.normalizeApiRecord = void 0;
6
+ exports.createApiMiddleware = exports.getApiConfig = exports.getSvcKey = exports.getSubdomain = void 0;
7
7
  const graphql_env_1 = require("@constructive-io/graphql-env");
8
8
  const logger_1 = require("@pgpmjs/logger");
9
9
  const server_utils_1 = require("@pgpmjs/server-utils");
10
10
  const url_domains_1 = require("@constructive-io/url-domains");
11
- const graphile_query_1 = require("graphile-query");
12
- const graphile_settings_1 = require("graphile-settings");
13
11
  const pg_cache_1 = require("pg-cache");
14
12
  const _50x_1 = __importDefault(require("../errors/50x"));
15
13
  const _404_message_1 = __importDefault(require("../errors/404-message"));
16
- /**
17
- * Normalizes API records into ApiStructure
18
- */
19
- const gql_1 = require("./gql");
20
- require("./types"); // for Request type
21
- var gql_2 = require("./gql");
22
- Object.defineProperty(exports, "normalizeApiRecord", { enumerable: true, get: function () { return gql_2.normalizeApiRecord; } });
14
+ require("./types");
23
15
  const log = new logger_1.Logger('api');
24
16
  const isDev = () => (0, graphql_env_1.getNodeEnv)() === 'development';
25
- const isApiError = (svc) => !!svc && typeof svc.errorHtml === 'string';
26
- const getPortFromRequest = (req) => {
27
- const host = req.headers.host;
28
- if (!host)
29
- return null;
30
- const parts = host.split(':');
31
- return parts.length === 2 ? `:${parts[1]}` : null;
32
- };
33
- const parseCommaSeparatedHeader = (value) => value
34
- .split(',')
35
- .map((item) => item.trim())
36
- .filter((item) => item.length > 0);
17
+ // =============================================================================
18
+ // SQL Queries
19
+ // =============================================================================
20
+ const DOMAIN_LOOKUP_SQL = `
21
+ SELECT
22
+ a.id as api_id,
23
+ a.database_id,
24
+ a.dbname,
25
+ a.role_name,
26
+ a.anon_role,
27
+ a.is_public,
28
+ COALESCE(array_agg(s.schema_name) FILTER (WHERE s.schema_name IS NOT NULL), '{}') as schemas
29
+ FROM services_public.domains d
30
+ JOIN services_public.apis a ON d.api_id = a.id
31
+ LEFT JOIN services_public.api_schemas aps ON a.id = aps.api_id
32
+ LEFT JOIN metaschema_public.schema s ON aps.schema_id = s.id
33
+ WHERE d.domain = $1
34
+ AND (($2::text IS NULL AND d.subdomain IS NULL) OR d.subdomain = $2)
35
+ AND a.is_public = $3
36
+ GROUP BY a.id, a.database_id, a.dbname, a.role_name, a.anon_role, a.is_public
37
+ LIMIT 1
38
+ `;
39
+ const API_NAME_LOOKUP_SQL = `
40
+ SELECT
41
+ a.id as api_id,
42
+ a.database_id,
43
+ a.dbname,
44
+ a.role_name,
45
+ a.anon_role,
46
+ a.is_public,
47
+ COALESCE(array_agg(s.schema_name) FILTER (WHERE s.schema_name IS NOT NULL), '{}') as schemas
48
+ FROM services_public.apis a
49
+ LEFT JOIN services_public.api_schemas aps ON a.id = aps.api_id
50
+ LEFT JOIN metaschema_public.schema s ON aps.schema_id = s.id
51
+ WHERE a.database_id = $1
52
+ AND a.name = $2
53
+ AND a.is_public = $3
54
+ GROUP BY a.id, a.database_id, a.dbname, a.role_name, a.anon_role, a.is_public
55
+ LIMIT 1
56
+ `;
57
+ const API_LIST_SQL = `
58
+ SELECT
59
+ a.id,
60
+ a.database_id,
61
+ a.name,
62
+ a.dbname,
63
+ a.role_name,
64
+ a.anon_role,
65
+ a.is_public,
66
+ COALESCE(
67
+ json_agg(
68
+ json_build_object('domain', d.domain, 'subdomain', d.subdomain)
69
+ ) FILTER (WHERE d.domain IS NOT NULL),
70
+ '[]'
71
+ ) as domains
72
+ FROM services_public.apis a
73
+ LEFT JOIN services_public.domains d ON a.id = d.api_id
74
+ WHERE a.is_public = $1
75
+ GROUP BY a.id, a.database_id, a.name, a.dbname, a.role_name, a.anon_role, a.is_public
76
+ LIMIT 100
77
+ `;
78
+ const RLS_MODULE_SQL = `
79
+ SELECT
80
+ rm.authenticate,
81
+ rm.authenticate_strict,
82
+ ps.schema_name as private_schema_name
83
+ FROM metaschema_modules_public.rls_module rm
84
+ LEFT JOIN metaschema_public.schema ps ON rm.private_schema_id = ps.id
85
+ WHERE rm.api_id = $1
86
+ LIMIT 1
87
+ `;
88
+ // =============================================================================
89
+ // Helpers
90
+ // =============================================================================
91
+ const isApiError = (result) => !!result && typeof result.errorHtml === 'string';
92
+ const parseCommaSeparatedHeader = (value) => value.split(',').map((s) => s.trim()).filter(Boolean);
37
93
  const getUrlDomains = (req) => {
38
94
  const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
39
95
  const parsed = (0, url_domains_1.parseUrl)(fullUrl);
@@ -42,297 +98,317 @@ const getUrlDomains = (req) => {
42
98
  subdomains: parsed.subdomains ?? [],
43
99
  };
44
100
  };
45
- const getSubdomain = (reqDomains) => {
46
- const names = reqDomains.filter((name) => !['www'].includes(name));
47
- return !names.length ? null : names.join('.');
101
+ const getSubdomain = (subdomains) => {
102
+ const filtered = subdomains.filter((name) => name !== 'www');
103
+ return filtered.length ? filtered.join('.') : null;
48
104
  };
49
105
  exports.getSubdomain = getSubdomain;
50
- const createApiMiddleware = (opts) => {
51
- return async (req, res, next) => {
52
- // Log incoming request details at debug level to avoid excessive info logs in production
53
- log.debug(`[api-middleware] Request: ${req.method} ${req.path}`);
54
- log.debug(`[api-middleware] Headers: X-Api-Name=${req.get('X-Api-Name')}, X-Database-Id=${req.get('X-Database-Id')}, X-Meta-Schema=${req.get('X-Meta-Schema')}, Host=${req.get('Host')}`);
55
- if (opts.api?.enableServicesApi === false) {
56
- const schemas = opts.api.exposedSchemas ?? [];
57
- const anonRole = opts.api.anonRole ?? '';
58
- const roleName = opts.api.roleName ?? '';
59
- const databaseId = opts.api.defaultDatabaseId;
60
- const api = {
61
- dbname: opts.pg?.database ?? '',
62
- anonRole,
63
- roleName,
64
- schema: schemas,
65
- apiModules: [],
66
- domains: [],
67
- databaseId,
68
- isPublic: false,
69
- };
70
- req.api = api;
71
- req.databaseId = databaseId;
72
- req.svc_key = 'meta-api-off';
73
- return next();
106
+ const getSvcKey = (opts, req) => {
107
+ const { domain, subdomains } = getUrlDomains(req);
108
+ const baseKey = subdomains.filter((n) => n !== 'www').concat(domain).join('.');
109
+ if (opts.api?.isPublic === false) {
110
+ if (req.get('X-Api-Name')) {
111
+ return `api:${req.get('X-Database-Id')}:${req.get('X-Api-Name')}`;
74
112
  }
75
- try {
76
- const apiConfig = await (0, exports.getApiConfig)(opts, req);
77
- if (isApiError(apiConfig)) {
78
- res
79
- .status(404)
80
- .send((0, _404_message_1.default)('API not found', apiConfig.errorHtml));
81
- return;
82
- }
83
- else if (!apiConfig) {
84
- res
85
- .status(404)
86
- .send((0, _404_message_1.default)('API service not found for the given domain/subdomain.'));
87
- return;
88
- }
89
- req.api = apiConfig;
90
- req.databaseId = apiConfig.databaseId;
91
- if (isDev())
92
- log.debug(`Resolved API: db=${apiConfig.dbname}, schemas=[${apiConfig.schema?.join(', ')}]`);
93
- next();
113
+ if (req.get('X-Schemata')) {
114
+ return `schemata:${req.get('X-Database-Id')}:${req.get('X-Schemata')}`;
94
115
  }
95
- catch (error) {
96
- const err = error;
97
- if (err.code === 'NO_VALID_SCHEMAS') {
98
- res.status(404).send((0, _404_message_1.default)(err.message));
99
- }
100
- else if (err.message?.match(/does not exist/)) {
101
- res
102
- .status(404)
103
- .send((0, _404_message_1.default)("The resource you're looking for does not exist."));
104
- }
105
- else {
106
- log.error('API middleware error:', err);
107
- res.status(500).send(_50x_1.default);
108
- }
116
+ if (req.get('X-Meta-Schema')) {
117
+ return `metaschema:api:${req.get('X-Database-Id')}`;
109
118
  }
119
+ }
120
+ return baseKey;
121
+ };
122
+ exports.getSvcKey = getSvcKey;
123
+ const toRlsModule = (row) => {
124
+ if (!row || !row.private_schema_name)
125
+ return undefined;
126
+ return {
127
+ authenticate: row.authenticate ?? undefined,
128
+ authenticateStrict: row.authenticate_strict ?? undefined,
129
+ privateSchema: {
130
+ schemaName: row.private_schema_name,
131
+ },
110
132
  };
111
133
  };
112
- exports.createApiMiddleware = createApiMiddleware;
113
- const createAdminApiStructure = ({ opts, schemata, key, databaseId, }) => {
114
- const api = {
134
+ const toApiStructure = (row, opts, rlsModuleRow) => ({
135
+ dbname: row.dbname || opts.pg?.database || '',
136
+ anonRole: row.anon_role || 'anon',
137
+ roleName: row.role_name || 'authenticated',
138
+ schema: row.schemas || [],
139
+ apiModules: [],
140
+ rlsModule: toRlsModule(rlsModuleRow ?? null),
141
+ domains: [],
142
+ databaseId: row.database_id,
143
+ isPublic: row.is_public,
144
+ });
145
+ const createAdminStructure = (opts, schemas, databaseId) => ({
146
+ dbname: opts.pg?.database ?? '',
147
+ anonRole: 'administrator',
148
+ roleName: 'administrator',
149
+ schema: schemas,
150
+ apiModules: [],
151
+ domains: [],
152
+ databaseId,
153
+ isPublic: false,
154
+ });
155
+ // =============================================================================
156
+ // Database Queries
157
+ // =============================================================================
158
+ const validateSchemata = async (pool, schemas) => {
159
+ const result = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name = ANY($1::text[])`, [schemas]);
160
+ return result.rows.map((row) => row.schema_name);
161
+ };
162
+ const queryByDomain = async (pool, domain, subdomain, isPublic) => {
163
+ const result = await pool.query(DOMAIN_LOOKUP_SQL, [domain, subdomain, isPublic]);
164
+ return result.rows[0] ?? null;
165
+ };
166
+ const queryByApiName = async (pool, databaseId, name, isPublic) => {
167
+ const result = await pool.query(API_NAME_LOOKUP_SQL, [databaseId, name, isPublic]);
168
+ return result.rows[0] ?? null;
169
+ };
170
+ const queryApiList = async (pool, isPublic) => {
171
+ const result = await pool.query(API_LIST_SQL, [isPublic]);
172
+ return result.rows;
173
+ };
174
+ const queryRlsModule = async (pool, apiId) => {
175
+ const result = await pool.query(RLS_MODULE_SQL, [apiId]);
176
+ return result.rows[0] ?? null;
177
+ };
178
+ // =============================================================================
179
+ // Resolution Logic
180
+ // =============================================================================
181
+ const determineMode = (ctx) => {
182
+ const { opts, headers } = ctx;
183
+ if (opts.api?.enableServicesApi === false)
184
+ return 'services-disabled';
185
+ if (opts.api?.isPublic === false) {
186
+ if (headers.schemata)
187
+ return 'schemata-header';
188
+ if (headers.apiName)
189
+ return 'api-name-header';
190
+ if (headers.metaSchema)
191
+ return 'meta-schema-header';
192
+ }
193
+ return 'domain-lookup';
194
+ };
195
+ const resolveServicesDisabled = (ctx) => {
196
+ const { opts } = ctx;
197
+ return {
115
198
  dbname: opts.pg?.database ?? '',
116
- anonRole: 'administrator',
117
- roleName: 'administrator',
118
- schema: schemata,
199
+ anonRole: opts.api?.anonRole ?? '',
200
+ roleName: opts.api?.roleName ?? '',
201
+ schema: opts.api?.exposedSchemas ?? [],
119
202
  apiModules: [],
120
203
  domains: [],
121
- databaseId,
204
+ databaseId: opts.api?.defaultDatabaseId,
122
205
  isPublic: false,
123
206
  };
124
- server_utils_1.svcCache.set(key, api);
125
- return api;
126
207
  };
127
- const queryServiceByDomainAndSubdomain = async ({ opts, key, domainModel, domain, subdomain, }) => {
128
- const where = {
129
- domain: { equalTo: domain },
130
- subdomain: subdomain === null || subdomain === undefined
131
- ? { isNull: true }
132
- : { equalTo: subdomain },
133
- };
134
- const result = await domainModel
135
- .findFirst({ select: gql_1.domainSelect, where })
136
- .execute();
137
- if (!result.ok) {
138
- log.error('GraphQL query errors:', result.errors);
139
- return null;
208
+ const resolveSchemataHeader = async (ctx, validatedSchemas) => {
209
+ const { opts, headers } = ctx;
210
+ const headerSchemas = parseCommaSeparatedHeader(headers.schemata);
211
+ const validSet = new Set(validatedSchemas);
212
+ const validHeaderSchemas = headerSchemas.filter((s) => validSet.has(s));
213
+ if (validHeaderSchemas.length === 0) {
214
+ return { errorHtml: 'No valid schemas found for the supplied X-Schemata header.' };
140
215
  }
141
- const domainRecord = result.data.domains.nodes[0];
142
- const api = domainRecord?.api;
143
- const apiPublic = opts.api?.isPublic;
144
- if (!api || api.isPublic !== apiPublic)
145
- return null;
146
- const apiStructure = (0, gql_1.normalizeApiRecord)(api);
147
- server_utils_1.svcCache.set(key, apiStructure);
148
- return apiStructure;
216
+ return createAdminStructure(opts, validHeaderSchemas, headers.databaseId);
149
217
  };
150
- const queryServiceByApiName = async ({ opts, key, queryOps, databaseId, name, }) => {
151
- if (!databaseId)
218
+ const resolveApiNameHeader = async (ctx) => {
219
+ const { opts, pool, headers } = ctx;
220
+ if (!headers.databaseId)
152
221
  return null;
153
- const result = await queryOps
154
- .apiByDatabaseIdAndName({ databaseId, name }, { select: gql_1.apiSelect })
155
- .execute();
156
- if (!result.ok) {
157
- log.error('GraphQL query errors:', result.errors);
222
+ const isPublic = opts.api?.isPublic ?? false;
223
+ const row = await queryByApiName(pool, headers.databaseId, headers.apiName, isPublic);
224
+ if (!row) {
225
+ log.debug(`[api-name-lookup] No API found for databaseId=${headers.databaseId} name=${headers.apiName}`);
158
226
  return null;
159
227
  }
160
- const api = result.data.apiByDatabaseIdAndName;
161
- const apiPublic = opts.api?.isPublic;
162
- if (api && api.isPublic === apiPublic) {
163
- const apiStructure = (0, gql_1.normalizeApiRecord)(api);
164
- server_utils_1.svcCache.set(key, apiStructure);
165
- return apiStructure;
166
- }
167
- return null;
228
+ const rlsModule = await queryRlsModule(pool, row.api_id);
229
+ log.debug(`[api-name-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${rlsModule ? 'found' : 'none'}`);
230
+ return toApiStructure(row, opts, rlsModule);
168
231
  };
169
- exports.queryServiceByApiName = queryServiceByApiName;
170
- const getSvcKey = (opts, req) => {
171
- const { domain, subdomains } = getUrlDomains(req);
172
- const key = subdomains
173
- .filter((name) => !['www'].includes(name))
174
- .concat(domain)
175
- .join('.');
176
- const apiPublic = opts.api?.isPublic;
177
- if (apiPublic === false) {
178
- if (req.get('X-Api-Name')) {
179
- return 'api:' + req.get('X-Database-Id') + ':' + req.get('X-Api-Name');
180
- }
181
- if (req.get('X-Schemata')) {
182
- return ('schemata:' + req.get('X-Database-Id') + ':' + req.get('X-Schemata'));
183
- }
184
- if (req.get('X-Meta-Schema')) {
185
- return 'metaschema:api:' + req.get('X-Database-Id');
186
- }
232
+ const resolveMetaSchemaHeader = (ctx, validatedSchemas) => {
233
+ return createAdminStructure(ctx.opts, validatedSchemas, ctx.headers.databaseId);
234
+ };
235
+ const resolveDomainLookup = async (ctx) => {
236
+ const { opts, pool, domain, subdomain } = ctx;
237
+ const isPublic = opts.api?.isPublic ?? false;
238
+ log.debug(`[domain-lookup] domain=${domain} subdomain=${subdomain} isPublic=${isPublic}`);
239
+ const row = await queryByDomain(pool, domain, subdomain, isPublic);
240
+ if (!row) {
241
+ log.debug(`[domain-lookup] No API found for domain=${domain} subdomain=${subdomain}`);
242
+ return null;
187
243
  }
188
- return key;
244
+ const rlsModule = await queryRlsModule(pool, row.api_id);
245
+ log.debug(`[domain-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${rlsModule ? 'found' : 'none'}`);
246
+ return toApiStructure(row, opts, rlsModule);
189
247
  };
190
- exports.getSvcKey = getSvcKey;
191
- const validateSchemata = async (pool, schemata) => {
192
- const result = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name = ANY($1::text[])`, [schemata]);
193
- return result.rows.map((row) => row.schema_name);
248
+ const buildDevFallbackError = async (ctx, req) => {
249
+ if ((0, graphql_env_1.getNodeEnv)() !== 'development')
250
+ return null;
251
+ const isPublic = ctx.opts.api?.isPublic ?? false;
252
+ const apis = await queryApiList(ctx.pool, isPublic);
253
+ if (!apis.length)
254
+ return null;
255
+ const host = req.get('host') || '';
256
+ const portMatch = host.match(/:(\d+)$/);
257
+ const port = portMatch ? portMatch[1] : '';
258
+ const apiCards = apis.map((api) => {
259
+ const domains = api.domains.length
260
+ ? api.domains.map((d) => {
261
+ const hostname = d.subdomain ? `${d.subdomain}.${d.domain}` : d.domain;
262
+ const url = port ? `http://${hostname}:${port}/graphiql` : `http://${hostname}/graphiql`;
263
+ return `<a href="${url}" style="color:#01A1FF;text-decoration:none;font-weight:500" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">${hostname}</a>`;
264
+ }).join('<span style="color:#D4DCEA;margin:0 4px">·</span>')
265
+ : '<span style="color:#8E9398;font-style:italic;font-size:11px">no domains</span>';
266
+ const badge = api.is_public
267
+ ? '<span style="color:#01A1FF;font-size:10px;font-weight:500">public</span>'
268
+ : '<span style="color:#8E9398;font-size:10px">private</span>';
269
+ return `
270
+ <div style="background:#fff;border-radius:8px;padding:10px 14px;margin-bottom:6px;box-shadow:0 1px 3px rgba(0,0,0,0.04);border:1px solid #E8ECF0;display:flex;align-items:center;gap:12px;transition:background 0.15s" onmouseover="this.style.background='#FAFBFC'" onmouseout="this.style.background='#fff'">
271
+ <div style="flex:1;min-width:0;display:flex;align-items:center;gap:8px;font-size:13px">
272
+ <span style="font-weight:600;color:#232323;white-space:nowrap">${api.name}</span>
273
+ <span style="color:#D4DCEA">→</span>
274
+ ${domains}
275
+ </div>
276
+ <div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
277
+ <span style="color:#8E9398;font-size:11px;font-family:'SF Mono',Monaco,monospace">${api.dbname}</span>
278
+ ${badge}
279
+ </div>
280
+ </div>`;
281
+ }).join('');
282
+ return {
283
+ errorHtml: `
284
+ <div style="text-align:left;max-width:600px;margin:0 auto">
285
+ <p style="color:#8E9398;font-size:11px;margin-bottom:10px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px">Available APIs</p>
286
+ ${apiCards}
287
+ </div>`,
288
+ };
194
289
  };
290
+ // =============================================================================
291
+ // Main Resolution Function
292
+ // =============================================================================
195
293
  const getApiConfig = async (opts, req) => {
196
- const rootPgPool = (0, pg_cache_1.getPgPool)(opts.pg);
294
+ const pool = (0, pg_cache_1.getPgPool)(opts.pg);
197
295
  const { domain, subdomains } = getUrlDomains(req);
198
296
  const subdomain = (0, exports.getSubdomain)(subdomains);
199
- const key = (0, exports.getSvcKey)(opts, req);
200
- req.svc_key = key;
201
- let apiConfig;
202
- if (server_utils_1.svcCache.has(key)) {
203
- if (isDev())
204
- log.debug(`Cache HIT for key=${key}`);
205
- apiConfig = server_utils_1.svcCache.get(key);
297
+ const cacheKey = (0, exports.getSvcKey)(opts, req);
298
+ req.svc_key = cacheKey;
299
+ // Check cache first
300
+ if (server_utils_1.svcCache.has(cacheKey)) {
301
+ log.debug(`Cache HIT for key=${cacheKey}`);
302
+ return server_utils_1.svcCache.get(cacheKey);
206
303
  }
207
- else {
208
- if (isDev())
209
- log.debug(`Cache MISS for key=${key}, looking up API`);
210
- const apiOpts = opts.api || {};
211
- const apiPublic = apiOpts.isPublic;
212
- const schemataHeader = req.get('X-Schemata');
213
- const apiNameHeader = req.get('X-Api-Name');
214
- const metaSchemaHeader = req.get('X-Meta-Schema');
215
- const databaseIdHeader = req.get('X-Database-Id');
216
- const headerSchemata = schemataHeader
217
- ? parseCommaSeparatedHeader(schemataHeader)
218
- : [];
219
- const candidateSchemata = apiPublic === false && headerSchemata.length
220
- ? Array.from(new Set([...(apiOpts.metaSchemas || []), ...headerSchemata]))
221
- : apiOpts.metaSchemas || [];
222
- const validatedSchemata = await validateSchemata(rootPgPool, candidateSchemata);
223
- if (validatedSchemata.length === 0) {
224
- const schemaSource = headerSchemata.length
225
- ? headerSchemata
226
- : apiOpts.metaSchemas || [];
227
- const label = headerSchemata.length ? 'X-Schemata' : 'metaSchemas';
228
- const message = `No valid schemas found. Configured ${label}: [${schemaSource.join(', ')}]`;
229
- if (isDev())
230
- log.debug(message);
231
- const error = new Error(message);
232
- error.code = 'NO_VALID_SCHEMAS';
233
- throw error;
234
- }
235
- const validSchemaSet = new Set(validatedSchemata);
236
- const validatedHeaderSchemata = headerSchemata.filter((schemaName) => validSchemaSet.has(schemaName));
237
- const settings = (0, graphile_settings_1.getGraphileSettings)({
238
- graphile: {
239
- schema: validatedSchemata,
240
- },
241
- });
242
- const graphileSettings = {
243
- ...settings,
244
- schema: validatedSchemata,
245
- };
246
- const schema = await (0, graphile_query_1.getSchema)(rootPgPool, graphileSettings);
247
- const graphileClient = new graphile_query_1.GraphileQuery({
248
- schema,
249
- pool: rootPgPool,
250
- settings: graphileSettings,
251
- });
252
- const orm = (0, gql_1.createGraphileOrm)(graphileClient);
253
- if (apiPublic === false) {
254
- if (schemataHeader) {
255
- if (validatedHeaderSchemata.length === 0) {
256
- return {
257
- errorHtml: 'No valid schemas found for the supplied X-Schemata header.',
258
- };
259
- }
260
- apiConfig = createAdminApiStructure({
261
- opts,
262
- schemata: validatedHeaderSchemata,
263
- key,
264
- databaseId: databaseIdHeader,
265
- });
266
- }
267
- else if (apiNameHeader) {
268
- apiConfig = await (0, exports.queryServiceByApiName)({
269
- opts,
270
- key,
271
- queryOps: orm.query,
272
- name: apiNameHeader,
273
- databaseId: databaseIdHeader,
274
- });
275
- }
276
- else if (metaSchemaHeader) {
277
- apiConfig = createAdminApiStructure({
278
- opts,
279
- schemata: validatedSchemata,
280
- key,
281
- databaseId: databaseIdHeader,
282
- });
283
- }
284
- else {
285
- apiConfig = await queryServiceByDomainAndSubdomain({
286
- opts,
287
- key,
288
- domainModel: orm.domain,
289
- domain,
290
- subdomain,
291
- });
304
+ log.debug(`Cache MISS for key=${cacheKey}, resolving API`);
305
+ const ctx = {
306
+ opts,
307
+ pool,
308
+ domain,
309
+ subdomain,
310
+ cacheKey,
311
+ headers: {
312
+ schemata: req.get('X-Schemata'),
313
+ apiName: req.get('X-Api-Name'),
314
+ metaSchema: req.get('X-Meta-Schema'),
315
+ databaseId: req.get('X-Database-Id'),
316
+ },
317
+ };
318
+ // Validate schemas upfront for modes that need them
319
+ const apiOpts = opts.api || {};
320
+ const headerSchemas = ctx.headers.schemata ? parseCommaSeparatedHeader(ctx.headers.schemata) : [];
321
+ const candidateSchemas = apiOpts.isPublic === false && headerSchemas.length
322
+ ? [...new Set([...(apiOpts.metaSchemas || []), ...headerSchemas])]
323
+ : apiOpts.metaSchemas || [];
324
+ const validatedSchemas = await validateSchemata(pool, candidateSchemas);
325
+ if (validatedSchemas.length === 0) {
326
+ const source = headerSchemas.length ? headerSchemas : apiOpts.metaSchemas || [];
327
+ const label = headerSchemas.length ? 'X-Schemata' : 'metaSchemas';
328
+ const error = new Error(`No valid schemas found. Configured ${label}: [${source.join(', ')}]`);
329
+ error.code = 'NO_VALID_SCHEMAS';
330
+ throw error;
331
+ }
332
+ // Route to appropriate resolver based on mode
333
+ const mode = determineMode(ctx);
334
+ let result;
335
+ switch (mode) {
336
+ case 'services-disabled':
337
+ result = resolveServicesDisabled(ctx);
338
+ break;
339
+ case 'schemata-header':
340
+ result = await resolveSchemataHeader(ctx, validatedSchemas);
341
+ break;
342
+ case 'api-name-header':
343
+ result = await resolveApiNameHeader(ctx);
344
+ break;
345
+ case 'meta-schema-header':
346
+ result = resolveMetaSchemaHeader(ctx, validatedSchemas);
347
+ break;
348
+ case 'domain-lookup':
349
+ result = await resolveDomainLookup(ctx);
350
+ if (!result && apiOpts.isPublic) {
351
+ const fallback = await buildDevFallbackError(ctx, req);
352
+ if (fallback)
353
+ return fallback;
292
354
  }
293
- }
294
- else {
295
- apiConfig = await queryServiceByDomainAndSubdomain({
355
+ break;
356
+ }
357
+ // Cache successful results
358
+ if (result && !isApiError(result)) {
359
+ server_utils_1.svcCache.set(cacheKey, result);
360
+ }
361
+ return result;
362
+ };
363
+ exports.getApiConfig = getApiConfig;
364
+ // =============================================================================
365
+ // Express Middleware
366
+ // =============================================================================
367
+ const createApiMiddleware = (opts) => {
368
+ return async (req, res, next) => {
369
+ log.debug(`[api-middleware] ${req.method} ${req.path}`);
370
+ // Fast path: services disabled
371
+ if (opts.api?.enableServicesApi === false) {
372
+ req.api = resolveServicesDisabled({
296
373
  opts,
297
- key,
298
- domainModel: orm.domain,
299
- domain,
300
- subdomain,
374
+ pool: null,
375
+ domain: '',
376
+ subdomain: null,
377
+ cacheKey: 'meta-api-off',
378
+ headers: {},
301
379
  });
380
+ req.databaseId = req.api.databaseId;
381
+ req.svc_key = 'meta-api-off';
382
+ return next();
383
+ }
384
+ try {
385
+ const apiConfig = await (0, exports.getApiConfig)(opts, req);
386
+ if (isApiError(apiConfig)) {
387
+ res.status(404).send((0, _404_message_1.default)('API not found', apiConfig.errorHtml));
388
+ return;
389
+ }
302
390
  if (!apiConfig) {
303
- // IMPORTANT NOTE: ONLY DO THIS IN DEV MODE
304
- if ((0, graphql_env_1.getNodeEnv)() === 'development') {
305
- const fallbackResult = await orm.api
306
- .findMany({ select: gql_1.apiListSelect, first: gql_1.connectionFirst })
307
- .execute();
308
- if (fallbackResult.ok && fallbackResult.data.apis.nodes.length) {
309
- const port = getPortFromRequest(req);
310
- const allDomains = fallbackResult.data.apis.nodes.flatMap((api) => api.domains.nodes.map((d) => ({
311
- domain: d.domain,
312
- subdomain: d.subdomain,
313
- href: d.subdomain
314
- ? `http://${d.subdomain}.${d.domain}${port}/graphiql`
315
- : `http://${d.domain}${port}/graphiql`,
316
- })));
317
- const linksHtml = allDomains.length
318
- ? `<ul class="mt-4 pl-5 list-disc space-y-1">` +
319
- allDomains
320
- .map((domainLink) => `<li><a href="${domainLink.href}" class="text-brand hover:underline">${domainLink.href}</a></li>`)
321
- .join('') +
322
- `</ul>`
323
- : `<p class="text-gray-600">No APIs are currently registered for this database.</p>`;
324
- const errorHtml = `
325
- <p class="text-sm text-gray-700">Try some of these:</p>
326
- <div class="mt-4">
327
- ${linksHtml}
328
- </div>
329
- `.trim();
330
- return { errorHtml };
331
- }
332
- }
391
+ res.status(404).send((0, _404_message_1.default)('API service not found for the given domain/subdomain.'));
392
+ return;
333
393
  }
394
+ req.api = apiConfig;
395
+ req.databaseId = apiConfig.databaseId;
396
+ log.debug(`Resolved API: db=${apiConfig.dbname}, schemas=[${apiConfig.schema?.join(', ')}]`);
397
+ next();
334
398
  }
335
- }
336
- return apiConfig;
399
+ catch (error) {
400
+ const err = error;
401
+ if (err.code === 'NO_VALID_SCHEMAS') {
402
+ res.status(404).send((0, _404_message_1.default)(err.message));
403
+ return;
404
+ }
405
+ if (err.message?.includes('does not exist')) {
406
+ res.status(404).send((0, _404_message_1.default)("The resource you're looking for does not exist."));
407
+ return;
408
+ }
409
+ log.error('API middleware error:', err);
410
+ res.status(500).send(_50x_1.default);
411
+ }
412
+ };
337
413
  };
338
- exports.getApiConfig = getApiConfig;
414
+ exports.createApiMiddleware = createApiMiddleware;