@constructive-io/graphql-server 2.10.5
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/LICENSE +23 -0
- package/README.md +89 -0
- package/errors/404-message.d.ts +2 -0
- package/errors/404-message.js +232 -0
- package/errors/404.d.ts +2 -0
- package/errors/404.js +218 -0
- package/errors/50x.d.ts +2 -0
- package/errors/50x.js +216 -0
- package/esm/errors/404-message.js +230 -0
- package/esm/errors/404.js +216 -0
- package/esm/errors/50x.js +214 -0
- package/esm/index.js +2 -0
- package/esm/middleware/api.js +337 -0
- package/esm/middleware/auth.js +68 -0
- package/esm/middleware/cors.js +63 -0
- package/esm/middleware/flush.js +49 -0
- package/esm/middleware/gql.js +125 -0
- package/esm/middleware/graphile.js +84 -0
- package/esm/middleware/types.js +1 -0
- package/esm/plugins/PublicKeySignature.js +114 -0
- package/esm/run.js +8 -0
- package/esm/schema.js +86 -0
- package/esm/scripts/create-bucket.js +32 -0
- package/esm/server.js +95 -0
- package/esm/types.js +1 -0
- package/index.d.ts +2 -0
- package/index.js +18 -0
- package/middleware/api.d.ts +6 -0
- package/middleware/api.js +346 -0
- package/middleware/auth.d.ts +4 -0
- package/middleware/auth.js +75 -0
- package/middleware/cors.d.ts +14 -0
- package/middleware/cors.js +70 -0
- package/middleware/flush.d.ts +5 -0
- package/middleware/flush.js +54 -0
- package/middleware/gql.d.ts +6 -0
- package/middleware/gql.js +131 -0
- package/middleware/graphile.d.ts +4 -0
- package/middleware/graphile.js +91 -0
- package/middleware/types.d.ts +33 -0
- package/middleware/types.js +2 -0
- package/package.json +88 -0
- package/plugins/PublicKeySignature.d.ts +11 -0
- package/plugins/PublicKeySignature.js +121 -0
- package/run.d.ts +2 -0
- package/run.js +10 -0
- package/schema.d.ts +12 -0
- package/schema.js +123 -0
- package/scripts/create-bucket.d.ts +1 -0
- package/scripts/create-bucket.js +34 -0
- package/server.d.ts +17 -0
- package/server.js +102 -0
- package/types.d.ts +85 -0
- package/types.js +2 -0
|
@@ -0,0 +1,346 @@
|
|
|
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.getApiConfig = exports.createApiMiddleware = exports.getSubdomain = void 0;
|
|
7
|
+
const graphql_env_1 = require("@constructive-io/graphql-env");
|
|
8
|
+
const server_utils_1 = require("@pgpmjs/server-utils");
|
|
9
|
+
const graphile_query_1 = require("graphile-query");
|
|
10
|
+
const graphile_settings_1 = require("graphile-settings");
|
|
11
|
+
const pg_cache_1 = require("pg-cache");
|
|
12
|
+
const _50x_1 = __importDefault(require("../errors/50x"));
|
|
13
|
+
const _404_message_1 = __importDefault(require("../errors/404-message"));
|
|
14
|
+
const gql_1 = require("./gql");
|
|
15
|
+
require("./types"); // for Request type
|
|
16
|
+
const transformServiceToApi = (svc) => {
|
|
17
|
+
const api = svc.data.api;
|
|
18
|
+
const schemaNames = api.schemaNamesFromExt?.nodes?.map((n) => n.schemaName) || [];
|
|
19
|
+
const additionalSchemas = api.schemaNames?.nodes?.map((n) => n.schemaName) || [];
|
|
20
|
+
let domains = [];
|
|
21
|
+
if (api.database?.sites?.nodes) {
|
|
22
|
+
domains = api.database.sites.nodes.reduce((acc, site) => {
|
|
23
|
+
if (site.domains?.nodes && site.domains.nodes.length) {
|
|
24
|
+
const siteUrls = site.domains.nodes.map((domain) => {
|
|
25
|
+
const hostname = domain.subdomain
|
|
26
|
+
? `${domain.subdomain}.${domain.domain}`
|
|
27
|
+
: domain.domain;
|
|
28
|
+
const protocol = domain.domain === 'localhost' ? 'http://' : 'https://';
|
|
29
|
+
return protocol + hostname;
|
|
30
|
+
});
|
|
31
|
+
return [...acc, ...siteUrls];
|
|
32
|
+
}
|
|
33
|
+
return acc;
|
|
34
|
+
}, []);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
dbname: api.dbname,
|
|
38
|
+
anonRole: api.anonRole,
|
|
39
|
+
roleName: api.roleName,
|
|
40
|
+
schema: [...schemaNames, ...additionalSchemas],
|
|
41
|
+
apiModules: api.apiModules?.nodes?.map((node) => ({
|
|
42
|
+
name: node.name,
|
|
43
|
+
data: node.data,
|
|
44
|
+
})) || [],
|
|
45
|
+
rlsModule: api.rlsModule,
|
|
46
|
+
domains,
|
|
47
|
+
databaseId: api.databaseId,
|
|
48
|
+
isPublic: api.isPublic,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const getPortFromRequest = (req) => {
|
|
52
|
+
const host = req.headers.host;
|
|
53
|
+
if (!host)
|
|
54
|
+
return null;
|
|
55
|
+
const parts = host.split(':');
|
|
56
|
+
return parts.length === 2 ? `:${parts[1]}` : null;
|
|
57
|
+
};
|
|
58
|
+
const getSubdomain = (reqDomains) => {
|
|
59
|
+
const names = reqDomains.filter((name) => !['www'].includes(name));
|
|
60
|
+
return !names.length ? null : names.join('.');
|
|
61
|
+
};
|
|
62
|
+
exports.getSubdomain = getSubdomain;
|
|
63
|
+
const createApiMiddleware = (opts) => {
|
|
64
|
+
return async (req, res, next) => {
|
|
65
|
+
if (opts.api?.enableMetaApi === false) {
|
|
66
|
+
const schemas = opts.api.exposedSchemas;
|
|
67
|
+
const anonRole = opts.api.anonRole;
|
|
68
|
+
const roleName = opts.api.roleName;
|
|
69
|
+
const databaseId = opts.api.defaultDatabaseId;
|
|
70
|
+
const api = {
|
|
71
|
+
dbname: opts.pg?.database ?? '',
|
|
72
|
+
anonRole,
|
|
73
|
+
roleName,
|
|
74
|
+
schema: schemas,
|
|
75
|
+
apiModules: [],
|
|
76
|
+
domains: [],
|
|
77
|
+
databaseId,
|
|
78
|
+
isPublic: false,
|
|
79
|
+
};
|
|
80
|
+
req.api = api;
|
|
81
|
+
req.databaseId = databaseId;
|
|
82
|
+
return next();
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const svc = await (0, exports.getApiConfig)(opts, req);
|
|
86
|
+
if (svc?.errorHtml) {
|
|
87
|
+
res
|
|
88
|
+
.status(404)
|
|
89
|
+
.send((0, _404_message_1.default)('API not found', svc.errorHtml));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
else if (!svc) {
|
|
93
|
+
res
|
|
94
|
+
.status(404)
|
|
95
|
+
.send((0, _404_message_1.default)('API service not found for the given domain/subdomain.'));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const api = transformServiceToApi(svc);
|
|
99
|
+
req.api = api;
|
|
100
|
+
req.databaseId = api.databaseId;
|
|
101
|
+
next();
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
if (e.code === 'NO_VALID_SCHEMAS') {
|
|
105
|
+
res.status(404).send((0, _404_message_1.default)(e.message));
|
|
106
|
+
}
|
|
107
|
+
else if (e.message.match(/does not exist/)) {
|
|
108
|
+
res
|
|
109
|
+
.status(404)
|
|
110
|
+
.send((0, _404_message_1.default)("The resource you're looking for does not exist."));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.error(e);
|
|
114
|
+
res.status(500).send(_50x_1.default);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
exports.createApiMiddleware = createApiMiddleware;
|
|
120
|
+
const getHardCodedSchemata = ({ opts, schemata, databaseId, key, }) => {
|
|
121
|
+
const svc = {
|
|
122
|
+
data: {
|
|
123
|
+
api: {
|
|
124
|
+
databaseId,
|
|
125
|
+
isPublic: false,
|
|
126
|
+
dbname: opts.pg.database,
|
|
127
|
+
anonRole: 'administrator',
|
|
128
|
+
roleName: 'administrator',
|
|
129
|
+
schemaNamesFromExt: {
|
|
130
|
+
nodes: schemata
|
|
131
|
+
.split(',')
|
|
132
|
+
.map((schema) => schema.trim())
|
|
133
|
+
.map((schemaName) => ({ schemaName })),
|
|
134
|
+
},
|
|
135
|
+
schemaNames: { nodes: [] },
|
|
136
|
+
apiModules: [],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
server_utils_1.svcCache.set(key, svc);
|
|
141
|
+
return svc;
|
|
142
|
+
};
|
|
143
|
+
const getMetaSchema = ({ opts, key, databaseId, }) => {
|
|
144
|
+
const apiOpts = opts.api || {};
|
|
145
|
+
const schemata = apiOpts.metaSchemas || [];
|
|
146
|
+
const svc = {
|
|
147
|
+
data: {
|
|
148
|
+
api: {
|
|
149
|
+
databaseId,
|
|
150
|
+
isPublic: false,
|
|
151
|
+
dbname: opts.pg.database,
|
|
152
|
+
anonRole: 'administrator',
|
|
153
|
+
roleName: 'administrator',
|
|
154
|
+
schemaNamesFromExt: {
|
|
155
|
+
nodes: schemata.map((schemaName) => ({ schemaName })),
|
|
156
|
+
},
|
|
157
|
+
schemaNames: { nodes: [] },
|
|
158
|
+
apiModules: [],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
server_utils_1.svcCache.set(key, svc);
|
|
163
|
+
return svc;
|
|
164
|
+
};
|
|
165
|
+
const queryServiceByDomainAndSubdomain = async ({ opts, key, client, domain, subdomain, }) => {
|
|
166
|
+
const result = await client.query({
|
|
167
|
+
role: 'administrator',
|
|
168
|
+
query: gql_1.ApiQuery,
|
|
169
|
+
variables: { domain, subdomain },
|
|
170
|
+
});
|
|
171
|
+
if (result.errors?.length) {
|
|
172
|
+
console.error(result.errors);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const nodes = result?.data?.domains?.nodes;
|
|
176
|
+
if (nodes?.length) {
|
|
177
|
+
const data = nodes[0];
|
|
178
|
+
const apiPublic = opts.api?.isPublic;
|
|
179
|
+
if (!data.api || data.api.isPublic !== apiPublic)
|
|
180
|
+
return null;
|
|
181
|
+
const svc = { data };
|
|
182
|
+
server_utils_1.svcCache.set(key, svc);
|
|
183
|
+
return svc;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
};
|
|
187
|
+
const queryServiceByApiName = async ({ opts, key, client, databaseId, name, }) => {
|
|
188
|
+
const result = await client.query({
|
|
189
|
+
role: 'administrator',
|
|
190
|
+
query: gql_1.ApiByNameQuery,
|
|
191
|
+
variables: { databaseId, name },
|
|
192
|
+
});
|
|
193
|
+
if (result.errors?.length) {
|
|
194
|
+
console.error(result.errors);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const data = result?.data;
|
|
198
|
+
const apiPublic = opts.api?.isPublic;
|
|
199
|
+
if (data?.api && data.api.isPublic === apiPublic) {
|
|
200
|
+
const svc = { data };
|
|
201
|
+
server_utils_1.svcCache.set(key, svc);
|
|
202
|
+
return svc;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
};
|
|
206
|
+
const getSvcKey = (opts, req) => {
|
|
207
|
+
const domain = req.urlDomains.domain;
|
|
208
|
+
const key = req.urlDomains.subdomains
|
|
209
|
+
.filter((name) => !['www'].includes(name))
|
|
210
|
+
.concat(domain)
|
|
211
|
+
.join('.');
|
|
212
|
+
const apiPublic = opts.api?.isPublic;
|
|
213
|
+
if (apiPublic === false) {
|
|
214
|
+
if (req.get('X-Api-Name')) {
|
|
215
|
+
return 'api:' + req.get('X-Database-Id') + ':' + req.get('X-Api-Name');
|
|
216
|
+
}
|
|
217
|
+
if (req.get('X-Schemata')) {
|
|
218
|
+
return ('schemata:' + req.get('X-Database-Id') + ':' + req.get('X-Schemata'));
|
|
219
|
+
}
|
|
220
|
+
if (req.get('X-Meta-Schema')) {
|
|
221
|
+
return 'metaschema:api:' + req.get('X-Database-Id');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return key;
|
|
225
|
+
};
|
|
226
|
+
const validateSchemata = async (pool, schemata) => {
|
|
227
|
+
const result = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name = ANY($1::text[])`, [schemata]);
|
|
228
|
+
return result.rows.map((row) => row.schema_name);
|
|
229
|
+
};
|
|
230
|
+
const getApiConfig = async (opts, req) => {
|
|
231
|
+
const rootPgPool = (0, pg_cache_1.getPgPool)(opts.pg);
|
|
232
|
+
// @ts-ignore
|
|
233
|
+
const subdomain = (0, exports.getSubdomain)(req.urlDomains.subdomains);
|
|
234
|
+
const domain = req.urlDomains.domain;
|
|
235
|
+
const key = getSvcKey(opts, req);
|
|
236
|
+
req.svc_key = key;
|
|
237
|
+
let svc;
|
|
238
|
+
if (server_utils_1.svcCache.has(key)) {
|
|
239
|
+
svc = server_utils_1.svcCache.get(key);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const apiOpts = opts.api || {};
|
|
243
|
+
const allSchemata = apiOpts.metaSchemas || [];
|
|
244
|
+
const validatedSchemata = await validateSchemata(rootPgPool, allSchemata);
|
|
245
|
+
if (validatedSchemata.length === 0) {
|
|
246
|
+
const message = `No valid schemas found for domain: ${domain}, subdomain: ${subdomain}`;
|
|
247
|
+
const error = new Error(message);
|
|
248
|
+
error.code = 'NO_VALID_SCHEMAS';
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
const settings = (0, graphile_settings_1.getGraphileSettings)({
|
|
252
|
+
graphile: {
|
|
253
|
+
schema: validatedSchemata,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
// @ts-ignore
|
|
257
|
+
const schema = await (0, graphile_query_1.getSchema)(rootPgPool, settings);
|
|
258
|
+
// @ts-ignore
|
|
259
|
+
const client = new graphile_query_1.GraphileQuery({ schema, pool: rootPgPool, settings });
|
|
260
|
+
const apiPublic = opts.api?.isPublic;
|
|
261
|
+
if (apiPublic === false) {
|
|
262
|
+
if (req.get('X-Schemata')) {
|
|
263
|
+
svc = getHardCodedSchemata({
|
|
264
|
+
opts,
|
|
265
|
+
key,
|
|
266
|
+
schemata: req.get('X-Schemata'),
|
|
267
|
+
databaseId: req.get('X-Database-Id'),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else if (req.get('X-Api-Name')) {
|
|
271
|
+
svc = await queryServiceByApiName({
|
|
272
|
+
opts,
|
|
273
|
+
key,
|
|
274
|
+
client,
|
|
275
|
+
name: req.get('X-Api-Name'),
|
|
276
|
+
databaseId: req.get('X-Database-Id'),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
else if (req.get('X-Meta-Schema')) {
|
|
280
|
+
svc = getMetaSchema({
|
|
281
|
+
opts,
|
|
282
|
+
key,
|
|
283
|
+
databaseId: req.get('X-Database-Id'),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
svc = await queryServiceByDomainAndSubdomain({
|
|
288
|
+
opts,
|
|
289
|
+
key,
|
|
290
|
+
client,
|
|
291
|
+
domain,
|
|
292
|
+
subdomain,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
svc = await queryServiceByDomainAndSubdomain({
|
|
298
|
+
opts,
|
|
299
|
+
key,
|
|
300
|
+
client,
|
|
301
|
+
domain,
|
|
302
|
+
subdomain,
|
|
303
|
+
});
|
|
304
|
+
if (!svc) {
|
|
305
|
+
if ((0, graphql_env_1.getNodeEnv)() === 'development') {
|
|
306
|
+
// TODO ONLY DO THIS IN DEV MODE
|
|
307
|
+
const fallbackResult = await client.query({
|
|
308
|
+
role: 'administrator',
|
|
309
|
+
// @ts-ignore
|
|
310
|
+
query: gql_1.ListOfAllDomainsOfDb,
|
|
311
|
+
// variables: { databaseId }
|
|
312
|
+
});
|
|
313
|
+
if (!fallbackResult.errors?.length &&
|
|
314
|
+
fallbackResult.data?.apis?.nodes?.length) {
|
|
315
|
+
const port = getPortFromRequest(req);
|
|
316
|
+
const allDomains = fallbackResult.data.apis.nodes.flatMap((api) => api.domains.nodes.map((d) => ({
|
|
317
|
+
domain: d.domain,
|
|
318
|
+
subdomain: d.subdomain,
|
|
319
|
+
href: d.subdomain
|
|
320
|
+
? `http://${d.subdomain}.${d.domain}${port}/graphiql`
|
|
321
|
+
: `http://${d.domain}${port}/graphiql`,
|
|
322
|
+
})));
|
|
323
|
+
const linksHtml = allDomains.length
|
|
324
|
+
? `<ul class="mt-4 pl-5 list-disc space-y-1">` +
|
|
325
|
+
allDomains
|
|
326
|
+
.map((d) => `<li><a href="${d.href}" class="text-brand hover:underline">${d.href}</a></li>`)
|
|
327
|
+
.join('') +
|
|
328
|
+
`</ul>`
|
|
329
|
+
: `<p class="text-gray-600">No APIs are currently registered for this database.</p>`;
|
|
330
|
+
const errorHtml = `
|
|
331
|
+
<p class="text-sm text-gray-700">Try some of these:</p>
|
|
332
|
+
<div class="mt-4">
|
|
333
|
+
${linksHtml}
|
|
334
|
+
</div>
|
|
335
|
+
`.trim();
|
|
336
|
+
return {
|
|
337
|
+
errorHtml,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return svc;
|
|
345
|
+
};
|
|
346
|
+
exports.getApiConfig = getApiConfig;
|
|
@@ -0,0 +1,75 @@
|
|
|
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.createAuthenticateMiddleware = void 0;
|
|
7
|
+
const pg_cache_1 = require("pg-cache");
|
|
8
|
+
const pg_query_context_1 = __importDefault(require("pg-query-context"));
|
|
9
|
+
require("./types"); // for Request type
|
|
10
|
+
const createAuthenticateMiddleware = (opts) => {
|
|
11
|
+
return async (req, res, next) => {
|
|
12
|
+
const api = req.api;
|
|
13
|
+
if (!api) {
|
|
14
|
+
res.status(500).send('Missing API info');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const pool = (0, pg_cache_1.getPgPool)({
|
|
18
|
+
...opts.pg,
|
|
19
|
+
database: api.dbname,
|
|
20
|
+
});
|
|
21
|
+
const rlsModule = api.rlsModule;
|
|
22
|
+
if (!rlsModule)
|
|
23
|
+
return next();
|
|
24
|
+
const authFn = opts.server.strictAuth
|
|
25
|
+
? rlsModule.authenticateStrict
|
|
26
|
+
: rlsModule.authenticate;
|
|
27
|
+
if (authFn && rlsModule.privateSchema.schemaName) {
|
|
28
|
+
const { authorization = '' } = req.headers;
|
|
29
|
+
const [authType, authToken] = authorization.split(' ');
|
|
30
|
+
let token = {};
|
|
31
|
+
if (authType?.toLowerCase() === 'bearer' && authToken) {
|
|
32
|
+
const context = {
|
|
33
|
+
'jwt.claims.ip_address': req.clientIp,
|
|
34
|
+
};
|
|
35
|
+
if (req.get('origin')) {
|
|
36
|
+
context['jwt.claims.origin'] = req.get('origin');
|
|
37
|
+
}
|
|
38
|
+
if (req.get('User-Agent')) {
|
|
39
|
+
context['jwt.claims.user_agent'] = req.get('User-Agent');
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const result = await (0, pg_query_context_1.default)({
|
|
43
|
+
client: pool,
|
|
44
|
+
context,
|
|
45
|
+
query: `SELECT * FROM "${rlsModule.privateSchema.schemaName}"."${authFn}"($1)`,
|
|
46
|
+
variables: [authToken],
|
|
47
|
+
});
|
|
48
|
+
if (result?.rowCount === 0) {
|
|
49
|
+
res.status(200).json({
|
|
50
|
+
errors: [{ extensions: { code: 'UNAUTHENTICATED' } }],
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
token = result.rows[0];
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
res.status(200).json({
|
|
58
|
+
errors: [
|
|
59
|
+
{
|
|
60
|
+
extensions: {
|
|
61
|
+
code: 'BAD_TOKEN_DEFINITION',
|
|
62
|
+
message: e.message,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
req.token = token;
|
|
71
|
+
}
|
|
72
|
+
next();
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
exports.createAuthenticateMiddleware = createAuthenticateMiddleware;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RequestHandler } from 'express';
|
|
2
|
+
import './types';
|
|
3
|
+
/**
|
|
4
|
+
* Unified CORS middleware for Constructive API
|
|
5
|
+
*
|
|
6
|
+
* Feature parity + compatibility:
|
|
7
|
+
* - Respects a global fallback origin (e.g. from env/CLI) for quick overrides.
|
|
8
|
+
* - Preserves multi-tenant, per-API CORS via meta schema ('cors' module + domains).
|
|
9
|
+
* - Always allows localhost to ease development.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* app.use(cors(fallbackOrigin));
|
|
13
|
+
*/
|
|
14
|
+
export declare const cors: (fallbackOrigin?: string) => RequestHandler;
|
|
@@ -0,0 +1,70 @@
|
|
|
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.cors = void 0;
|
|
7
|
+
const url_domains_1 = require("@constructive-io/url-domains");
|
|
8
|
+
const cors_1 = __importDefault(require("cors"));
|
|
9
|
+
require("./types"); // for Request type
|
|
10
|
+
/**
|
|
11
|
+
* Unified CORS middleware for Constructive API
|
|
12
|
+
*
|
|
13
|
+
* Feature parity + compatibility:
|
|
14
|
+
* - Respects a global fallback origin (e.g. from env/CLI) for quick overrides.
|
|
15
|
+
* - Preserves multi-tenant, per-API CORS via meta schema ('cors' module + domains).
|
|
16
|
+
* - Always allows localhost to ease development.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* app.use(cors(fallbackOrigin));
|
|
20
|
+
*/
|
|
21
|
+
const cors = (fallbackOrigin) => {
|
|
22
|
+
// Use the cors library's dynamic origin function to decide per request
|
|
23
|
+
const dynamicOrigin = (origin, callback, req) => {
|
|
24
|
+
// 1) Global fallback (fast path)
|
|
25
|
+
if (fallbackOrigin && fallbackOrigin.trim().length) {
|
|
26
|
+
if (fallbackOrigin.trim() === '*') {
|
|
27
|
+
// Reflect whatever Origin the caller sent
|
|
28
|
+
return callback(null, true);
|
|
29
|
+
}
|
|
30
|
+
if (origin && origin.trim() === fallbackOrigin.trim()) {
|
|
31
|
+
return callback(null, true);
|
|
32
|
+
}
|
|
33
|
+
// If a strict fallback origin is provided and does not match,
|
|
34
|
+
// continue to per-API checks below (do not immediately deny).
|
|
35
|
+
}
|
|
36
|
+
// 2) Per-API allowlist sourced from req.api (if available)
|
|
37
|
+
// createApiMiddleware runs before this in server.ts, so req.api should be set
|
|
38
|
+
const api = req.api;
|
|
39
|
+
if (api) {
|
|
40
|
+
const corsModules = (api.apiModules || []).filter((m) => m.name === 'cors');
|
|
41
|
+
const siteUrls = api.domains || [];
|
|
42
|
+
const listOfDomains = corsModules.reduce((m, mod) => [...mod.data.urls, ...m], siteUrls);
|
|
43
|
+
if (origin && listOfDomains.includes(origin)) {
|
|
44
|
+
return callback(null, true);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// 3) Localhost is always allowed
|
|
48
|
+
if (origin) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = (0, url_domains_1.parseUrl)(new URL(origin));
|
|
51
|
+
if (parsed.domain === 'localhost') {
|
|
52
|
+
return callback(null, true);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// ignore invalid origin
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Default: not allowed
|
|
60
|
+
return callback(null, false);
|
|
61
|
+
};
|
|
62
|
+
// Wrap in the cors plugin with our dynamic origin resolver
|
|
63
|
+
const handler = (req, res, next) => (0, cors_1.default)({
|
|
64
|
+
origin: (reqOrigin, cb) => dynamicOrigin(reqOrigin, cb, req),
|
|
65
|
+
credentials: true,
|
|
66
|
+
optionsSuccessStatus: 200,
|
|
67
|
+
})(req, res, next);
|
|
68
|
+
return handler;
|
|
69
|
+
};
|
|
70
|
+
exports.cors = cors;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ConstructiveOptions } from '@constructive-io/graphql-types';
|
|
2
|
+
import { NextFunction, Request, Response } from 'express';
|
|
3
|
+
import './types';
|
|
4
|
+
export declare const flush: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
5
|
+
export declare const flushService: (opts: ConstructiveOptions, databaseId: string) => Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.flushService = exports.flush = void 0;
|
|
4
|
+
const logger_1 = require("@pgpmjs/logger");
|
|
5
|
+
const server_utils_1 = require("@pgpmjs/server-utils");
|
|
6
|
+
const graphile_cache_1 = require("graphile-cache");
|
|
7
|
+
const pg_cache_1 = require("pg-cache");
|
|
8
|
+
require("./types"); // for Request type
|
|
9
|
+
const log = new logger_1.Logger('flush');
|
|
10
|
+
const flush = async (req, res, next) => {
|
|
11
|
+
if (req.url === '/flush') {
|
|
12
|
+
// TODO: check bearer for a flush / special key
|
|
13
|
+
graphile_cache_1.graphileCache.delete(req.svc_key);
|
|
14
|
+
server_utils_1.svcCache.delete(req.svc_key);
|
|
15
|
+
res.status(200).send('OK');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
return next();
|
|
19
|
+
};
|
|
20
|
+
exports.flush = flush;
|
|
21
|
+
const flushService = async (opts, databaseId) => {
|
|
22
|
+
const pgPool = (0, pg_cache_1.getPgPool)(opts.pg);
|
|
23
|
+
log.info('flushing db ' + databaseId);
|
|
24
|
+
const api = new RegExp(`^api:${databaseId}:.*`);
|
|
25
|
+
const schemata = new RegExp(`^schemata:${databaseId}:.*`);
|
|
26
|
+
const meta = new RegExp(`^metaschema:api:${databaseId}`);
|
|
27
|
+
if (!opts.api.isPublic) {
|
|
28
|
+
graphile_cache_1.graphileCache.forEach((_, k) => {
|
|
29
|
+
if (api.test(k) || schemata.test(k) || meta.test(k)) {
|
|
30
|
+
graphile_cache_1.graphileCache.delete(k);
|
|
31
|
+
server_utils_1.svcCache.delete(k);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const svc = await pgPool.query(`SELECT *
|
|
36
|
+
FROM meta_public.domains
|
|
37
|
+
WHERE database_id = $1`, [databaseId]);
|
|
38
|
+
if (svc.rowCount === 0)
|
|
39
|
+
return;
|
|
40
|
+
for (const row of svc.rows) {
|
|
41
|
+
let key;
|
|
42
|
+
if (row.domain && !row.subdomain) {
|
|
43
|
+
key = row.domain;
|
|
44
|
+
}
|
|
45
|
+
else if (row.domain && row.subdomain) {
|
|
46
|
+
key = `${row.subdomain}.${row.domain}`;
|
|
47
|
+
}
|
|
48
|
+
if (key) {
|
|
49
|
+
graphile_cache_1.graphileCache.delete(key);
|
|
50
|
+
server_utils_1.svcCache.delete(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
exports.flushService = flushService;
|