@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.
Files changed (54) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +89 -0
  3. package/errors/404-message.d.ts +2 -0
  4. package/errors/404-message.js +232 -0
  5. package/errors/404.d.ts +2 -0
  6. package/errors/404.js +218 -0
  7. package/errors/50x.d.ts +2 -0
  8. package/errors/50x.js +216 -0
  9. package/esm/errors/404-message.js +230 -0
  10. package/esm/errors/404.js +216 -0
  11. package/esm/errors/50x.js +214 -0
  12. package/esm/index.js +2 -0
  13. package/esm/middleware/api.js +337 -0
  14. package/esm/middleware/auth.js +68 -0
  15. package/esm/middleware/cors.js +63 -0
  16. package/esm/middleware/flush.js +49 -0
  17. package/esm/middleware/gql.js +125 -0
  18. package/esm/middleware/graphile.js +84 -0
  19. package/esm/middleware/types.js +1 -0
  20. package/esm/plugins/PublicKeySignature.js +114 -0
  21. package/esm/run.js +8 -0
  22. package/esm/schema.js +86 -0
  23. package/esm/scripts/create-bucket.js +32 -0
  24. package/esm/server.js +95 -0
  25. package/esm/types.js +1 -0
  26. package/index.d.ts +2 -0
  27. package/index.js +18 -0
  28. package/middleware/api.d.ts +6 -0
  29. package/middleware/api.js +346 -0
  30. package/middleware/auth.d.ts +4 -0
  31. package/middleware/auth.js +75 -0
  32. package/middleware/cors.d.ts +14 -0
  33. package/middleware/cors.js +70 -0
  34. package/middleware/flush.d.ts +5 -0
  35. package/middleware/flush.js +54 -0
  36. package/middleware/gql.d.ts +6 -0
  37. package/middleware/gql.js +131 -0
  38. package/middleware/graphile.d.ts +4 -0
  39. package/middleware/graphile.js +91 -0
  40. package/middleware/types.d.ts +33 -0
  41. package/middleware/types.js +2 -0
  42. package/package.json +88 -0
  43. package/plugins/PublicKeySignature.d.ts +11 -0
  44. package/plugins/PublicKeySignature.js +121 -0
  45. package/run.d.ts +2 -0
  46. package/run.js +10 -0
  47. package/schema.d.ts +12 -0
  48. package/schema.js +123 -0
  49. package/scripts/create-bucket.d.ts +1 -0
  50. package/scripts/create-bucket.js +34 -0
  51. package/server.d.ts +17 -0
  52. package/server.js +102 -0
  53. package/types.d.ts +85 -0
  54. package/types.js +2 -0
@@ -0,0 +1,214 @@
1
+ export default `
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">
6
+ <META NAME="ROBOTS" CONTENT="NOINDEX, NOFOLLOW">
7
+ <META NAME="GOOGLEBOT" CONTENT="NOARCHIVE">
8
+ <title>Uh Oh</title>
9
+
10
+ <link href='//fonts.googleapis.com/css2?family=Fjalla+One&display=swap' rel='stylesheet' type='text/css'>
11
+
12
+ <style type="text/css">
13
+ .fade-in-cls {
14
+ -webkit-animation: fade-in 2s 0.2s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275);
15
+ -moz-animation: fade-in 2s 0.2s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275);
16
+ animation: fade-in 2s 0.2s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275);
17
+ -webkit-transform: translateY(10px);
18
+ -moz-transform: translateY(10px);
19
+ -o-transform: translateY(10px);
20
+ transform: translateY(10px);
21
+ -webkit-opacity: 0;
22
+ -moz-opacity: 0;
23
+ opacity: 0;
24
+ }
25
+
26
+ @-webkit-keyframes fade-in {
27
+ 100% {
28
+ -webkit-transform: translateY(0px);
29
+ -moz-transform: translateY(0px);
30
+ -o-transform: translateY(0px);
31
+ transform: translateY(0px);
32
+ -webkit-opacity: 1;
33
+ -moz-opacity: 1;
34
+ opacity: 1;
35
+ }
36
+ }
37
+
38
+ @-moz-keyframes fade-in {
39
+ 100% {
40
+ -webkit-transform: translateY(0px);
41
+ -moz-transform: translateY(0px);
42
+ -o-transform: translateY(0px);
43
+ transform: translateY(0px);
44
+ -webkit-opacity: 1;
45
+ -moz-opacity: 1;
46
+ opacity: 1;
47
+ }
48
+ }
49
+
50
+ @keyframes fade-in {
51
+ 100% {
52
+ -webkit-transform: translateY(0px);
53
+ -moz-transform: translateY(0px);
54
+ -o-transform: translateY(0px);
55
+ transform: translateY(0px);
56
+ -webkit-opacity: 1;
57
+ -moz-opacity: 1;
58
+ opacity: 1;
59
+ }
60
+ }
61
+
62
+ .time {
63
+
64
+ -webkit-animation: ckw 15s infinite;
65
+ /* Safari 4+ */
66
+ -moz-animation: ckw 15s infinite;
67
+ /* Fx 5+ */
68
+ -o-animation: ckw 15s infinite;
69
+ /* Opera 12+ */
70
+ animation: ckw 15s infinite;
71
+ /* IE 10+, Fx 29+ */
72
+ -webkit-animation-timing-function: linear;
73
+ /* Chrome, Safari, Opera */
74
+ animation-timing-function: linear;
75
+ transform-origin: 50% 50%;
76
+ display: inline-block;
77
+ /* <--- */
78
+ }
79
+
80
+ @keyframes ckw {
81
+ 0% {
82
+ transform: rotate(0deg);
83
+ -webkit-transform: rotate(0deg);
84
+ -moz-transform: rotate(0deg);
85
+ -o-transform: rotate(0deg);
86
+ }
87
+
88
+ 100% {
89
+ transform: rotate(360deg);
90
+ -webkit-transform: rotate(360deg);
91
+ -moz-transform: rotate(360deg);
92
+ -o-transform: rotate(360deg);
93
+ }
94
+ }
95
+
96
+ @-webkit-keyframes ckw {
97
+ 0% {
98
+ transform: rotate(0deg);
99
+ -webkit-transform: rotate(0deg);
100
+ -moz-transform: rotate(0deg);
101
+ -o-transform: rotate(0deg);
102
+ }
103
+
104
+ 100% {
105
+ transform: rotate(360deg);
106
+ -webkit-transform: rotate(360deg);
107
+ -moz-transform: rotate(360deg);
108
+ -o-transform: rotate(360deg);
109
+ }
110
+ }
111
+
112
+ @-moz-keyframes ckw {
113
+ 0% {
114
+ transform: rotate(0deg);
115
+ -webkit-transform: rotate(0deg);
116
+ -moz-transform: rotate(0deg);
117
+ -o-transform: rotate(0deg);
118
+ }
119
+
120
+ 100% {
121
+ transform: rotate(360deg);
122
+ -webkit-transform: rotate(360deg);
123
+ -moz-transform: rotate(360deg);
124
+ -o-transform: rotate(360deg);
125
+ }
126
+ }
127
+
128
+ body {
129
+ background-color: #dde7e9;
130
+ color: #01A1FF;
131
+ font-family: 'Fjalla One', sans-serif;
132
+ position: relative;
133
+ margin: 0px;
134
+ }
135
+
136
+ section {
137
+ width: 100%;
138
+ height: 100%;
139
+ position: absolute;
140
+ }
141
+
142
+ article {
143
+ display: table;
144
+ width: 100%;
145
+ height: 100%;
146
+ }
147
+
148
+ /* .border-top {
149
+ width: 95px;
150
+ background-color: #fff;
151
+ height: 2px;
152
+ display: inline-block;
153
+ margin: 0px auto;
154
+ } */
155
+
156
+ .vcntr {
157
+ display: table-cell;
158
+ height: 100%;
159
+ width: 100%;
160
+ vertical-align: middle;
161
+ }
162
+
163
+ .logo {
164
+ width: 100px;
165
+ margin: 10px auto;
166
+ display: block;
167
+ }
168
+
169
+ h1 {
170
+ font-size: 21px;
171
+ line-height: 32px;
172
+ margin-bottom: 0px;
173
+ margin-top: 28px;
174
+ font-weight: 700;
175
+ text-transform: uppercase;
176
+ letter-spacing: 2px;
177
+ }
178
+
179
+ p {
180
+ font-size: 16px;
181
+ line-height: 23px;
182
+ margin-bottom: 8px;
183
+ margin-top: 6px;
184
+ }
185
+
186
+ .textc {
187
+ text-align: center;
188
+ }
189
+
190
+ @media only screen and (max-width:480px) {
191
+ .logo {
192
+ width: 100px;
193
+ }
194
+ }
195
+ </style>
196
+ </head>
197
+
198
+ <body class="">
199
+ <section>
200
+ <article>
201
+ <div class="vcntr">
202
+ <div class="logo fade-in-cls">
203
+ <svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64.95 61.24"><path d="M16.36 43.47a14.56 14.56 0 01-8.79 0A11.16 11.16 0 011 38.34a8.83 8.83 0 01-.45-1A8.09 8.09 0 01.76 31a10.08 10.08 0 013.68-4.17 6.23 6.23 0 01-.24-.65A8.17 8.17 0 014.68 20a10.47 10.47 0 014.53-4.53 13.25 13.25 0 012.12-.9 12.35 12.35 0 012.5-7.45A17.35 17.35 0 0122.7 1a22.38 22.38 0 0113.48 0 17.07 17.07 0 019.42 6.9 18.75 18.75 0 019.8.4 14 14 0 018.29 6.46 10.17 10.17 0 01.78 1.83 10 10 0 01-.59 7.56 13.05 13.05 0 01-5.64 5.64A17.05 17.05 0 0155.4 31l-39 12.45zM64.92 36q-1.05 4.48-10.54 7.63l-.36.12L15 56.22a9.87 9.87 0 01-1.91.41C6.88 57.55 2.16 54.78 0 51.24a8.2 8.2 0 00.56 3.2 7.45 7.45 0 00.45 1 11.09 11.09 0 006.56 5.13 14.64 14.64 0 008.79 0l39-12.45a17.07 17.07 0 002.84-1.2 13 13 0 005.64-5.63 10.19 10.19 0 001-5.27zm0-8.59q-1.05 4.49-10.54 7.64l-.36.12L15 47.64a9.84 9.84 0 01-1.91.4C6.88 49 2.16 46.19 0 42.65a8.28 8.28 0 00.56 3.21 8.83 8.83 0 00.45 1A11.16 11.16 0 007.57 52a14.64 14.64 0 008.79 0l39-12.45a17.05 17.05 0 002.84-1.19 13.05 13.05 0 005.64-5.64 10.17 10.17 0 001-5.27zM9 39.13a10.11 10.11 0 006 0l39-12.45a12.25 12.25 0 002.06-.87 8.52 8.52 0 003.7-3.62 5.52 5.52 0 00.37-4.19 6.6 6.6 0 00-.46-1A9.57 9.57 0 0054 12.68a14.17 14.17 0 00-8.48 0l-.16.05-.2.07-2 .69-.84-2c-.07-.16-.12-.27-.14-.33l-.18-.27a12.26 12.26 0 00-7.2-5.51 17.85 17.85 0 00-10.73 0 12.81 12.81 0 00-6.56 4.44 7.58 7.58 0 00-1.47 6l.45 2.29-2.31.4c-.27 0-.5.09-.68.13s-.46.12-.66.19a8.31 8.31 0 00-1.47.62A5.88 5.88 0 008.78 22a3.71 3.71 0 00-.24 2.79 3.77 3.77 0 00.31.7c.07.13.15.26.24.4.1.14.19.27.29.39l2 2.44-3 1.13a9.92 9.92 0 00-.94.44 5.79 5.79 0 00-2.59 2.57 3.57 3.57 0 00-.1 2.83 4.9 4.9 0 00.22.48 6.69 6.69 0 003.92 3z" fill="#01a1ff"/></svg>
204
+ </div>
205
+ <div class="textc">
206
+ <h1>Uh Oh!</h1>
207
+ <p>We’re really sorry about that. Please contact support of the issue persists.</p>
208
+ </div>
209
+ </div>
210
+ </article>
211
+ </section>
212
+ </body>
213
+
214
+ </html>`;
package/esm/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './server';
2
+ export * from './schema';
@@ -0,0 +1,337 @@
1
+ import { getNodeEnv } from '@constructive-io/graphql-env';
2
+ import { svcCache } from '@pgpmjs/server-utils';
3
+ import { getSchema, GraphileQuery } from 'graphile-query';
4
+ import { getGraphileSettings } from 'graphile-settings';
5
+ import { getPgPool } from 'pg-cache';
6
+ import errorPage50x from '../errors/50x';
7
+ import errorPage404Message from '../errors/404-message';
8
+ import { ApiByNameQuery, ApiQuery, ListOfAllDomainsOfDb } from './gql';
9
+ import './types'; // for Request type
10
+ const transformServiceToApi = (svc) => {
11
+ const api = svc.data.api;
12
+ const schemaNames = api.schemaNamesFromExt?.nodes?.map((n) => n.schemaName) || [];
13
+ const additionalSchemas = api.schemaNames?.nodes?.map((n) => n.schemaName) || [];
14
+ let domains = [];
15
+ if (api.database?.sites?.nodes) {
16
+ domains = api.database.sites.nodes.reduce((acc, site) => {
17
+ if (site.domains?.nodes && site.domains.nodes.length) {
18
+ const siteUrls = site.domains.nodes.map((domain) => {
19
+ const hostname = domain.subdomain
20
+ ? `${domain.subdomain}.${domain.domain}`
21
+ : domain.domain;
22
+ const protocol = domain.domain === 'localhost' ? 'http://' : 'https://';
23
+ return protocol + hostname;
24
+ });
25
+ return [...acc, ...siteUrls];
26
+ }
27
+ return acc;
28
+ }, []);
29
+ }
30
+ return {
31
+ dbname: api.dbname,
32
+ anonRole: api.anonRole,
33
+ roleName: api.roleName,
34
+ schema: [...schemaNames, ...additionalSchemas],
35
+ apiModules: api.apiModules?.nodes?.map((node) => ({
36
+ name: node.name,
37
+ data: node.data,
38
+ })) || [],
39
+ rlsModule: api.rlsModule,
40
+ domains,
41
+ databaseId: api.databaseId,
42
+ isPublic: api.isPublic,
43
+ };
44
+ };
45
+ const getPortFromRequest = (req) => {
46
+ const host = req.headers.host;
47
+ if (!host)
48
+ return null;
49
+ const parts = host.split(':');
50
+ return parts.length === 2 ? `:${parts[1]}` : null;
51
+ };
52
+ export const getSubdomain = (reqDomains) => {
53
+ const names = reqDomains.filter((name) => !['www'].includes(name));
54
+ return !names.length ? null : names.join('.');
55
+ };
56
+ export const createApiMiddleware = (opts) => {
57
+ return async (req, res, next) => {
58
+ if (opts.api?.enableMetaApi === false) {
59
+ const schemas = opts.api.exposedSchemas;
60
+ const anonRole = opts.api.anonRole;
61
+ const roleName = opts.api.roleName;
62
+ const databaseId = opts.api.defaultDatabaseId;
63
+ const api = {
64
+ dbname: opts.pg?.database ?? '',
65
+ anonRole,
66
+ roleName,
67
+ schema: schemas,
68
+ apiModules: [],
69
+ domains: [],
70
+ databaseId,
71
+ isPublic: false,
72
+ };
73
+ req.api = api;
74
+ req.databaseId = databaseId;
75
+ return next();
76
+ }
77
+ try {
78
+ const svc = await getApiConfig(opts, req);
79
+ if (svc?.errorHtml) {
80
+ res
81
+ .status(404)
82
+ .send(errorPage404Message('API not found', svc.errorHtml));
83
+ return;
84
+ }
85
+ else if (!svc) {
86
+ res
87
+ .status(404)
88
+ .send(errorPage404Message('API service not found for the given domain/subdomain.'));
89
+ return;
90
+ }
91
+ const api = transformServiceToApi(svc);
92
+ req.api = api;
93
+ req.databaseId = api.databaseId;
94
+ next();
95
+ }
96
+ catch (e) {
97
+ if (e.code === 'NO_VALID_SCHEMAS') {
98
+ res.status(404).send(errorPage404Message(e.message));
99
+ }
100
+ else if (e.message.match(/does not exist/)) {
101
+ res
102
+ .status(404)
103
+ .send(errorPage404Message("The resource you're looking for does not exist."));
104
+ }
105
+ else {
106
+ console.error(e);
107
+ res.status(500).send(errorPage50x);
108
+ }
109
+ }
110
+ };
111
+ };
112
+ const getHardCodedSchemata = ({ opts, schemata, databaseId, key, }) => {
113
+ const svc = {
114
+ data: {
115
+ api: {
116
+ databaseId,
117
+ isPublic: false,
118
+ dbname: opts.pg.database,
119
+ anonRole: 'administrator',
120
+ roleName: 'administrator',
121
+ schemaNamesFromExt: {
122
+ nodes: schemata
123
+ .split(',')
124
+ .map((schema) => schema.trim())
125
+ .map((schemaName) => ({ schemaName })),
126
+ },
127
+ schemaNames: { nodes: [] },
128
+ apiModules: [],
129
+ },
130
+ },
131
+ };
132
+ svcCache.set(key, svc);
133
+ return svc;
134
+ };
135
+ const getMetaSchema = ({ opts, key, databaseId, }) => {
136
+ const apiOpts = opts.api || {};
137
+ const schemata = apiOpts.metaSchemas || [];
138
+ const svc = {
139
+ data: {
140
+ api: {
141
+ databaseId,
142
+ isPublic: false,
143
+ dbname: opts.pg.database,
144
+ anonRole: 'administrator',
145
+ roleName: 'administrator',
146
+ schemaNamesFromExt: {
147
+ nodes: schemata.map((schemaName) => ({ schemaName })),
148
+ },
149
+ schemaNames: { nodes: [] },
150
+ apiModules: [],
151
+ },
152
+ },
153
+ };
154
+ svcCache.set(key, svc);
155
+ return svc;
156
+ };
157
+ const queryServiceByDomainAndSubdomain = async ({ opts, key, client, domain, subdomain, }) => {
158
+ const result = await client.query({
159
+ role: 'administrator',
160
+ query: ApiQuery,
161
+ variables: { domain, subdomain },
162
+ });
163
+ if (result.errors?.length) {
164
+ console.error(result.errors);
165
+ return null;
166
+ }
167
+ const nodes = result?.data?.domains?.nodes;
168
+ if (nodes?.length) {
169
+ const data = nodes[0];
170
+ const apiPublic = opts.api?.isPublic;
171
+ if (!data.api || data.api.isPublic !== apiPublic)
172
+ return null;
173
+ const svc = { data };
174
+ svcCache.set(key, svc);
175
+ return svc;
176
+ }
177
+ return null;
178
+ };
179
+ const queryServiceByApiName = async ({ opts, key, client, databaseId, name, }) => {
180
+ const result = await client.query({
181
+ role: 'administrator',
182
+ query: ApiByNameQuery,
183
+ variables: { databaseId, name },
184
+ });
185
+ if (result.errors?.length) {
186
+ console.error(result.errors);
187
+ return null;
188
+ }
189
+ const data = result?.data;
190
+ const apiPublic = opts.api?.isPublic;
191
+ if (data?.api && data.api.isPublic === apiPublic) {
192
+ const svc = { data };
193
+ svcCache.set(key, svc);
194
+ return svc;
195
+ }
196
+ return null;
197
+ };
198
+ const getSvcKey = (opts, req) => {
199
+ const domain = req.urlDomains.domain;
200
+ const key = req.urlDomains.subdomains
201
+ .filter((name) => !['www'].includes(name))
202
+ .concat(domain)
203
+ .join('.');
204
+ const apiPublic = opts.api?.isPublic;
205
+ if (apiPublic === false) {
206
+ if (req.get('X-Api-Name')) {
207
+ return 'api:' + req.get('X-Database-Id') + ':' + req.get('X-Api-Name');
208
+ }
209
+ if (req.get('X-Schemata')) {
210
+ return ('schemata:' + req.get('X-Database-Id') + ':' + req.get('X-Schemata'));
211
+ }
212
+ if (req.get('X-Meta-Schema')) {
213
+ return 'metaschema:api:' + req.get('X-Database-Id');
214
+ }
215
+ }
216
+ return key;
217
+ };
218
+ const validateSchemata = async (pool, schemata) => {
219
+ const result = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name = ANY($1::text[])`, [schemata]);
220
+ return result.rows.map((row) => row.schema_name);
221
+ };
222
+ export const getApiConfig = async (opts, req) => {
223
+ const rootPgPool = getPgPool(opts.pg);
224
+ // @ts-ignore
225
+ const subdomain = getSubdomain(req.urlDomains.subdomains);
226
+ const domain = req.urlDomains.domain;
227
+ const key = getSvcKey(opts, req);
228
+ req.svc_key = key;
229
+ let svc;
230
+ if (svcCache.has(key)) {
231
+ svc = svcCache.get(key);
232
+ }
233
+ else {
234
+ const apiOpts = opts.api || {};
235
+ const allSchemata = apiOpts.metaSchemas || [];
236
+ const validatedSchemata = await validateSchemata(rootPgPool, allSchemata);
237
+ if (validatedSchemata.length === 0) {
238
+ const message = `No valid schemas found for domain: ${domain}, subdomain: ${subdomain}`;
239
+ const error = new Error(message);
240
+ error.code = 'NO_VALID_SCHEMAS';
241
+ throw error;
242
+ }
243
+ const settings = getGraphileSettings({
244
+ graphile: {
245
+ schema: validatedSchemata,
246
+ },
247
+ });
248
+ // @ts-ignore
249
+ const schema = await getSchema(rootPgPool, settings);
250
+ // @ts-ignore
251
+ const client = new GraphileQuery({ schema, pool: rootPgPool, settings });
252
+ const apiPublic = opts.api?.isPublic;
253
+ if (apiPublic === false) {
254
+ if (req.get('X-Schemata')) {
255
+ svc = getHardCodedSchemata({
256
+ opts,
257
+ key,
258
+ schemata: req.get('X-Schemata'),
259
+ databaseId: req.get('X-Database-Id'),
260
+ });
261
+ }
262
+ else if (req.get('X-Api-Name')) {
263
+ svc = await queryServiceByApiName({
264
+ opts,
265
+ key,
266
+ client,
267
+ name: req.get('X-Api-Name'),
268
+ databaseId: req.get('X-Database-Id'),
269
+ });
270
+ }
271
+ else if (req.get('X-Meta-Schema')) {
272
+ svc = getMetaSchema({
273
+ opts,
274
+ key,
275
+ databaseId: req.get('X-Database-Id'),
276
+ });
277
+ }
278
+ else {
279
+ svc = await queryServiceByDomainAndSubdomain({
280
+ opts,
281
+ key,
282
+ client,
283
+ domain,
284
+ subdomain,
285
+ });
286
+ }
287
+ }
288
+ else {
289
+ svc = await queryServiceByDomainAndSubdomain({
290
+ opts,
291
+ key,
292
+ client,
293
+ domain,
294
+ subdomain,
295
+ });
296
+ if (!svc) {
297
+ if (getNodeEnv() === 'development') {
298
+ // TODO ONLY DO THIS IN DEV MODE
299
+ const fallbackResult = await client.query({
300
+ role: 'administrator',
301
+ // @ts-ignore
302
+ query: ListOfAllDomainsOfDb,
303
+ // variables: { databaseId }
304
+ });
305
+ if (!fallbackResult.errors?.length &&
306
+ fallbackResult.data?.apis?.nodes?.length) {
307
+ const port = getPortFromRequest(req);
308
+ const allDomains = fallbackResult.data.apis.nodes.flatMap((api) => api.domains.nodes.map((d) => ({
309
+ domain: d.domain,
310
+ subdomain: d.subdomain,
311
+ href: d.subdomain
312
+ ? `http://${d.subdomain}.${d.domain}${port}/graphiql`
313
+ : `http://${d.domain}${port}/graphiql`,
314
+ })));
315
+ const linksHtml = allDomains.length
316
+ ? `<ul class="mt-4 pl-5 list-disc space-y-1">` +
317
+ allDomains
318
+ .map((d) => `<li><a href="${d.href}" class="text-brand hover:underline">${d.href}</a></li>`)
319
+ .join('') +
320
+ `</ul>`
321
+ : `<p class="text-gray-600">No APIs are currently registered for this database.</p>`;
322
+ const errorHtml = `
323
+ <p class="text-sm text-gray-700">Try some of these:</p>
324
+ <div class="mt-4">
325
+ ${linksHtml}
326
+ </div>
327
+ `.trim();
328
+ return {
329
+ errorHtml,
330
+ };
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+ return svc;
337
+ };
@@ -0,0 +1,68 @@
1
+ import { getPgPool } from 'pg-cache';
2
+ import pgQueryContext from 'pg-query-context';
3
+ import './types'; // for Request type
4
+ export const createAuthenticateMiddleware = (opts) => {
5
+ return async (req, res, next) => {
6
+ const api = req.api;
7
+ if (!api) {
8
+ res.status(500).send('Missing API info');
9
+ return;
10
+ }
11
+ const pool = getPgPool({
12
+ ...opts.pg,
13
+ database: api.dbname,
14
+ });
15
+ const rlsModule = api.rlsModule;
16
+ if (!rlsModule)
17
+ return next();
18
+ const authFn = opts.server.strictAuth
19
+ ? rlsModule.authenticateStrict
20
+ : rlsModule.authenticate;
21
+ if (authFn && rlsModule.privateSchema.schemaName) {
22
+ const { authorization = '' } = req.headers;
23
+ const [authType, authToken] = authorization.split(' ');
24
+ let token = {};
25
+ if (authType?.toLowerCase() === 'bearer' && authToken) {
26
+ const context = {
27
+ 'jwt.claims.ip_address': req.clientIp,
28
+ };
29
+ if (req.get('origin')) {
30
+ context['jwt.claims.origin'] = req.get('origin');
31
+ }
32
+ if (req.get('User-Agent')) {
33
+ context['jwt.claims.user_agent'] = req.get('User-Agent');
34
+ }
35
+ try {
36
+ const result = await pgQueryContext({
37
+ client: pool,
38
+ context,
39
+ query: `SELECT * FROM "${rlsModule.privateSchema.schemaName}"."${authFn}"($1)`,
40
+ variables: [authToken],
41
+ });
42
+ if (result?.rowCount === 0) {
43
+ res.status(200).json({
44
+ errors: [{ extensions: { code: 'UNAUTHENTICATED' } }],
45
+ });
46
+ return;
47
+ }
48
+ token = result.rows[0];
49
+ }
50
+ catch (e) {
51
+ res.status(200).json({
52
+ errors: [
53
+ {
54
+ extensions: {
55
+ code: 'BAD_TOKEN_DEFINITION',
56
+ message: e.message,
57
+ },
58
+ },
59
+ ],
60
+ });
61
+ return;
62
+ }
63
+ }
64
+ req.token = token;
65
+ }
66
+ next();
67
+ };
68
+ };
@@ -0,0 +1,63 @@
1
+ import { parseUrl } from '@constructive-io/url-domains';
2
+ import corsPlugin from 'cors';
3
+ import './types'; // for Request type
4
+ /**
5
+ * Unified CORS middleware for Constructive API
6
+ *
7
+ * Feature parity + compatibility:
8
+ * - Respects a global fallback origin (e.g. from env/CLI) for quick overrides.
9
+ * - Preserves multi-tenant, per-API CORS via meta schema ('cors' module + domains).
10
+ * - Always allows localhost to ease development.
11
+ *
12
+ * Usage:
13
+ * app.use(cors(fallbackOrigin));
14
+ */
15
+ export const cors = (fallbackOrigin) => {
16
+ // Use the cors library's dynamic origin function to decide per request
17
+ const dynamicOrigin = (origin, callback, req) => {
18
+ // 1) Global fallback (fast path)
19
+ if (fallbackOrigin && fallbackOrigin.trim().length) {
20
+ if (fallbackOrigin.trim() === '*') {
21
+ // Reflect whatever Origin the caller sent
22
+ return callback(null, true);
23
+ }
24
+ if (origin && origin.trim() === fallbackOrigin.trim()) {
25
+ return callback(null, true);
26
+ }
27
+ // If a strict fallback origin is provided and does not match,
28
+ // continue to per-API checks below (do not immediately deny).
29
+ }
30
+ // 2) Per-API allowlist sourced from req.api (if available)
31
+ // createApiMiddleware runs before this in server.ts, so req.api should be set
32
+ const api = req.api;
33
+ if (api) {
34
+ const corsModules = (api.apiModules || []).filter((m) => m.name === 'cors');
35
+ const siteUrls = api.domains || [];
36
+ const listOfDomains = corsModules.reduce((m, mod) => [...mod.data.urls, ...m], siteUrls);
37
+ if (origin && listOfDomains.includes(origin)) {
38
+ return callback(null, true);
39
+ }
40
+ }
41
+ // 3) Localhost is always allowed
42
+ if (origin) {
43
+ try {
44
+ const parsed = parseUrl(new URL(origin));
45
+ if (parsed.domain === 'localhost') {
46
+ return callback(null, true);
47
+ }
48
+ }
49
+ catch {
50
+ // ignore invalid origin
51
+ }
52
+ }
53
+ // Default: not allowed
54
+ return callback(null, false);
55
+ };
56
+ // Wrap in the cors plugin with our dynamic origin resolver
57
+ const handler = (req, res, next) => corsPlugin({
58
+ origin: (reqOrigin, cb) => dynamicOrigin(reqOrigin, cb, req),
59
+ credentials: true,
60
+ optionsSuccessStatus: 200,
61
+ })(req, res, next);
62
+ return handler;
63
+ };