@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/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.
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.
|
|
35
|
-
.
|
|
36
|
-
|
|
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 = (
|
|
46
|
-
const
|
|
47
|
-
return
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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: '
|
|
117
|
-
roleName: '
|
|
118
|
-
schema:
|
|
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
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
|
151
|
-
|
|
218
|
+
const resolveApiNameHeader = async (ctx) => {
|
|
219
|
+
const { opts, pool, headers } = ctx;
|
|
220
|
+
if (!headers.databaseId)
|
|
152
221
|
return null;
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
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
|
|
200
|
-
req.svc_key =
|
|
201
|
-
|
|
202
|
-
if (server_utils_1.svcCache.has(
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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.
|
|
414
|
+
exports.createApiMiddleware = createApiMiddleware;
|