@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,49 @@
|
|
|
1
|
+
import { Logger } from '@pgpmjs/logger';
|
|
2
|
+
import { svcCache } from '@pgpmjs/server-utils';
|
|
3
|
+
import { graphileCache } from 'graphile-cache';
|
|
4
|
+
import { getPgPool } from 'pg-cache';
|
|
5
|
+
import './types'; // for Request type
|
|
6
|
+
const log = new Logger('flush');
|
|
7
|
+
export const flush = async (req, res, next) => {
|
|
8
|
+
if (req.url === '/flush') {
|
|
9
|
+
// TODO: check bearer for a flush / special key
|
|
10
|
+
graphileCache.delete(req.svc_key);
|
|
11
|
+
svcCache.delete(req.svc_key);
|
|
12
|
+
res.status(200).send('OK');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
return next();
|
|
16
|
+
};
|
|
17
|
+
export const flushService = async (opts, databaseId) => {
|
|
18
|
+
const pgPool = getPgPool(opts.pg);
|
|
19
|
+
log.info('flushing db ' + databaseId);
|
|
20
|
+
const api = new RegExp(`^api:${databaseId}:.*`);
|
|
21
|
+
const schemata = new RegExp(`^schemata:${databaseId}:.*`);
|
|
22
|
+
const meta = new RegExp(`^metaschema:api:${databaseId}`);
|
|
23
|
+
if (!opts.api.isPublic) {
|
|
24
|
+
graphileCache.forEach((_, k) => {
|
|
25
|
+
if (api.test(k) || schemata.test(k) || meta.test(k)) {
|
|
26
|
+
graphileCache.delete(k);
|
|
27
|
+
svcCache.delete(k);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const svc = await pgPool.query(`SELECT *
|
|
32
|
+
FROM meta_public.domains
|
|
33
|
+
WHERE database_id = $1`, [databaseId]);
|
|
34
|
+
if (svc.rowCount === 0)
|
|
35
|
+
return;
|
|
36
|
+
for (const row of svc.rows) {
|
|
37
|
+
let key;
|
|
38
|
+
if (row.domain && !row.subdomain) {
|
|
39
|
+
key = row.domain;
|
|
40
|
+
}
|
|
41
|
+
else if (row.domain && row.subdomain) {
|
|
42
|
+
key = `${row.subdomain}.${row.domain}`;
|
|
43
|
+
}
|
|
44
|
+
if (key) {
|
|
45
|
+
graphileCache.delete(key);
|
|
46
|
+
svcCache.delete(key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import gql from 'graphql-tag';
|
|
2
|
+
// DO NOT CHANGE TO domainBySubdomainAndDomain(domain: $domain, subdomain: $subdomain)
|
|
3
|
+
// condition is the way to handle since it will pass in null properly
|
|
4
|
+
// e.g. subdomain.domain or domain both work
|
|
5
|
+
export const ApiQuery = gql `
|
|
6
|
+
query ApiRoot($domain: String!, $subdomain: String) {
|
|
7
|
+
domains(condition: { domain: $domain, subdomain: $subdomain }) {
|
|
8
|
+
nodes {
|
|
9
|
+
api {
|
|
10
|
+
databaseId
|
|
11
|
+
dbname
|
|
12
|
+
roleName
|
|
13
|
+
anonRole
|
|
14
|
+
isPublic
|
|
15
|
+
schemaNamesFromExt: apiExtensions {
|
|
16
|
+
nodes {
|
|
17
|
+
schemaName
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
schemaNames: schemataByApiSchemaApiIdAndSchemaId {
|
|
21
|
+
nodes {
|
|
22
|
+
schemaName
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
rlsModule {
|
|
26
|
+
privateSchema {
|
|
27
|
+
schemaName
|
|
28
|
+
}
|
|
29
|
+
authenticateStrict
|
|
30
|
+
authenticate
|
|
31
|
+
currentRole
|
|
32
|
+
currentRoleId
|
|
33
|
+
}
|
|
34
|
+
database {
|
|
35
|
+
sites {
|
|
36
|
+
nodes {
|
|
37
|
+
domains {
|
|
38
|
+
nodes {
|
|
39
|
+
subdomain
|
|
40
|
+
domain
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} # for now keep this for patches
|
|
46
|
+
apiModules {
|
|
47
|
+
nodes {
|
|
48
|
+
name
|
|
49
|
+
data
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
export const ApiByNameQuery = gql `
|
|
58
|
+
query ApiByName($name: String!, $databaseId: UUID!) {
|
|
59
|
+
api: apiByDatabaseIdAndName(name: $name, databaseId: $databaseId) {
|
|
60
|
+
databaseId
|
|
61
|
+
dbname
|
|
62
|
+
roleName
|
|
63
|
+
anonRole
|
|
64
|
+
isPublic
|
|
65
|
+
schemaNamesFromExt: apiExtensions {
|
|
66
|
+
nodes {
|
|
67
|
+
schemaName
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
schemaNames: schemataByApiSchemaApiIdAndSchemaId {
|
|
71
|
+
nodes {
|
|
72
|
+
schemaName
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
rlsModule {
|
|
76
|
+
privateSchema {
|
|
77
|
+
schemaName
|
|
78
|
+
}
|
|
79
|
+
authenticate
|
|
80
|
+
authenticateStrict
|
|
81
|
+
currentRole
|
|
82
|
+
currentRoleId
|
|
83
|
+
}
|
|
84
|
+
database {
|
|
85
|
+
sites {
|
|
86
|
+
nodes {
|
|
87
|
+
domains {
|
|
88
|
+
nodes {
|
|
89
|
+
subdomain
|
|
90
|
+
domain
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} # for now keep this for patches
|
|
96
|
+
apiModules {
|
|
97
|
+
nodes {
|
|
98
|
+
name
|
|
99
|
+
data
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
export const ListOfAllDomainsOfDb = gql `
|
|
106
|
+
query ListApisByDatabaseId {
|
|
107
|
+
apis {
|
|
108
|
+
nodes {
|
|
109
|
+
id
|
|
110
|
+
databaseId
|
|
111
|
+
name
|
|
112
|
+
dbname
|
|
113
|
+
roleName
|
|
114
|
+
anonRole
|
|
115
|
+
isPublic
|
|
116
|
+
domains {
|
|
117
|
+
nodes {
|
|
118
|
+
domain
|
|
119
|
+
subdomain
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
`;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { graphileCache } from 'graphile-cache';
|
|
2
|
+
import { getGraphileSettings as getSettings } from 'graphile-settings';
|
|
3
|
+
import { getPgPool } from 'pg-cache';
|
|
4
|
+
import { postgraphile } from 'postgraphile';
|
|
5
|
+
import './types'; // for Request type
|
|
6
|
+
import PublicKeySignature from '../plugins/PublicKeySignature';
|
|
7
|
+
export const graphile = (opts) => {
|
|
8
|
+
return async (req, res, next) => {
|
|
9
|
+
try {
|
|
10
|
+
const api = req.api;
|
|
11
|
+
if (!api) {
|
|
12
|
+
return res.status(500).send('Missing API info');
|
|
13
|
+
}
|
|
14
|
+
const key = req.svc_key;
|
|
15
|
+
if (!key) {
|
|
16
|
+
return res.status(500).send('Missing service cache key');
|
|
17
|
+
}
|
|
18
|
+
const { dbname, anonRole, roleName, schema } = api;
|
|
19
|
+
if (graphileCache.has(key)) {
|
|
20
|
+
const { handler } = graphileCache.get(key);
|
|
21
|
+
return handler(req, res, next);
|
|
22
|
+
}
|
|
23
|
+
const options = getSettings({
|
|
24
|
+
...opts,
|
|
25
|
+
graphile: {
|
|
26
|
+
...opts.graphile,
|
|
27
|
+
schema: schema,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const pubkey_challenge = api.apiModules.find((mod) => mod.name === 'pubkey_challenge');
|
|
31
|
+
if (pubkey_challenge && pubkey_challenge.data) {
|
|
32
|
+
options.appendPlugins.push(PublicKeySignature(pubkey_challenge.data));
|
|
33
|
+
}
|
|
34
|
+
options.appendPlugins = options.appendPlugins ?? [];
|
|
35
|
+
options.appendPlugins.push(...opts.graphile.appendPlugins);
|
|
36
|
+
options.pgSettings = async function pgSettings(request) {
|
|
37
|
+
const gqlReq = request;
|
|
38
|
+
const context = {
|
|
39
|
+
[`jwt.claims.database_id`]: gqlReq.databaseId,
|
|
40
|
+
[`jwt.claims.ip_address`]: gqlReq.clientIp,
|
|
41
|
+
};
|
|
42
|
+
if (gqlReq.get('origin')) {
|
|
43
|
+
context['jwt.claims.origin'] = gqlReq.get('origin');
|
|
44
|
+
}
|
|
45
|
+
if (gqlReq.get('User-Agent')) {
|
|
46
|
+
context['jwt.claims.user_agent'] = gqlReq.get('User-Agent');
|
|
47
|
+
}
|
|
48
|
+
if (gqlReq?.token?.user_id) {
|
|
49
|
+
return {
|
|
50
|
+
role: roleName,
|
|
51
|
+
[`jwt.claims.token_id`]: gqlReq.token.id,
|
|
52
|
+
[`jwt.claims.user_id`]: gqlReq.token.user_id,
|
|
53
|
+
...context,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { role: anonRole, ...context };
|
|
57
|
+
};
|
|
58
|
+
options.graphqlRoute = '/graphql';
|
|
59
|
+
options.graphiqlRoute = '/graphiql';
|
|
60
|
+
options.graphileBuildOptions = {
|
|
61
|
+
...options.graphileBuildOptions,
|
|
62
|
+
...opts.graphile.graphileBuildOptions,
|
|
63
|
+
};
|
|
64
|
+
const graphileOpts = {
|
|
65
|
+
...options,
|
|
66
|
+
...opts.graphile.overrideSettings,
|
|
67
|
+
};
|
|
68
|
+
const pgPool = getPgPool({
|
|
69
|
+
...opts.pg,
|
|
70
|
+
database: dbname,
|
|
71
|
+
});
|
|
72
|
+
const handler = postgraphile(pgPool, schema, graphileOpts);
|
|
73
|
+
graphileCache.set(key, {
|
|
74
|
+
pgPool,
|
|
75
|
+
pgPoolKey: dbname,
|
|
76
|
+
handler,
|
|
77
|
+
});
|
|
78
|
+
return handler(req, res, next);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return res.status(500).send(e.message);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { gql, makeExtendSchemaPlugin } from 'graphile-utils';
|
|
2
|
+
import pgQueryWithContext from 'pg-query-context';
|
|
3
|
+
export const PublicKeySignature = (pubkey_challenge) => {
|
|
4
|
+
const { schema, crypto_network, sign_up_with_key, sign_in_request_challenge, sign_in_record_failure, sign_in_with_challenge } = pubkey_challenge;
|
|
5
|
+
return makeExtendSchemaPlugin(() => ({
|
|
6
|
+
typeDefs: gql `
|
|
7
|
+
input CreateUserAccountWithPublicKeyInput {
|
|
8
|
+
publicKey: String!
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
input GetMessageForSigningInput {
|
|
12
|
+
publicKey: String!
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
input VerifyMessageForSigningInput {
|
|
16
|
+
publicKey: String!
|
|
17
|
+
message: String!
|
|
18
|
+
signature: String!
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type createUserAccountWithPublicKeyPayload {
|
|
22
|
+
message: String!
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type getMessageForSigningPayload {
|
|
26
|
+
message: String!
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type verifyMessageForSigningPayload {
|
|
30
|
+
access_token: String!
|
|
31
|
+
access_token_expires_at: Datetime!
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
extend type Mutation {
|
|
35
|
+
createUserAccountWithPublicKey(
|
|
36
|
+
input: CreateUserAccountWithPublicKeyInput
|
|
37
|
+
): createUserAccountWithPublicKeyPayload
|
|
38
|
+
|
|
39
|
+
getMessageForSigning(
|
|
40
|
+
input: GetMessageForSigningInput
|
|
41
|
+
): getMessageForSigningPayload
|
|
42
|
+
|
|
43
|
+
verifyMessageForSigning(
|
|
44
|
+
input: VerifyMessageForSigningInput
|
|
45
|
+
): verifyMessageForSigningPayload
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
resolvers: {
|
|
49
|
+
Mutation: {
|
|
50
|
+
async createUserAccountWithPublicKey(_parent, args, context) {
|
|
51
|
+
const { pgClient } = context;
|
|
52
|
+
const { publicKey } = args.input;
|
|
53
|
+
await pgQueryWithContext({
|
|
54
|
+
client: pgClient,
|
|
55
|
+
context: { role: 'anonymous' },
|
|
56
|
+
query: `SELECT * FROM "${schema}".${sign_up_with_key}($1)`,
|
|
57
|
+
variables: [publicKey]
|
|
58
|
+
});
|
|
59
|
+
const { rows: [{ [sign_in_request_challenge]: message }] } = await pgQueryWithContext({
|
|
60
|
+
client: pgClient,
|
|
61
|
+
context: { role: 'anonymous' },
|
|
62
|
+
query: `SELECT * FROM "${schema}".${sign_in_request_challenge}($1)`,
|
|
63
|
+
variables: [publicKey]
|
|
64
|
+
});
|
|
65
|
+
return { message };
|
|
66
|
+
},
|
|
67
|
+
async getMessageForSigning(_parent, args, context) {
|
|
68
|
+
const { pgClient } = context;
|
|
69
|
+
const { publicKey } = args.input;
|
|
70
|
+
const { rows: [{ [sign_in_request_challenge]: message }] } = await pgQueryWithContext({
|
|
71
|
+
client: pgClient,
|
|
72
|
+
context: { role: 'anonymous' },
|
|
73
|
+
query: `SELECT * FROM "${schema}".${sign_in_request_challenge}($1)`,
|
|
74
|
+
variables: [publicKey]
|
|
75
|
+
});
|
|
76
|
+
if (!message)
|
|
77
|
+
throw new Error('NO_ACCOUNT_EXISTS');
|
|
78
|
+
return { message };
|
|
79
|
+
},
|
|
80
|
+
async verifyMessageForSigning(_parent, args, context) {
|
|
81
|
+
const { pgClient } = context;
|
|
82
|
+
const { publicKey, message, signature } = args.input;
|
|
83
|
+
// const network = Networks[crypto_network];
|
|
84
|
+
const network = 'btc';
|
|
85
|
+
// const result = verifyMessage(message, publicKey, signature, network);
|
|
86
|
+
// TODO implement using interchainJS?
|
|
87
|
+
const result = false;
|
|
88
|
+
if (!result) {
|
|
89
|
+
await pgQueryWithContext({
|
|
90
|
+
client: pgClient,
|
|
91
|
+
context: { role: 'anonymous' },
|
|
92
|
+
query: `SELECT * FROM "${schema}".${sign_in_record_failure}($1)`,
|
|
93
|
+
variables: [publicKey]
|
|
94
|
+
});
|
|
95
|
+
throw new Error('BAD_SIGNIN');
|
|
96
|
+
}
|
|
97
|
+
const { rows: [token] } = await pgQueryWithContext({
|
|
98
|
+
client: pgClient,
|
|
99
|
+
context: { role: 'anonymous' },
|
|
100
|
+
query: `SELECT * FROM "${schema}".${sign_in_with_challenge}($1, $2)`,
|
|
101
|
+
variables: [publicKey, message]
|
|
102
|
+
});
|
|
103
|
+
if (!token?.access_token)
|
|
104
|
+
throw new Error('BAD_SIGNIN');
|
|
105
|
+
return {
|
|
106
|
+
access_token: token.access_token,
|
|
107
|
+
access_token_expires_at: token.access_token_expires_at
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}));
|
|
113
|
+
};
|
|
114
|
+
export default PublicKeySignature;
|
package/esm/run.js
ADDED
package/esm/schema.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { printSchema, getIntrospectionQuery, buildClientSchema } from 'graphql';
|
|
2
|
+
import { getGraphileSettings } from 'graphile-settings';
|
|
3
|
+
import { getPgPool } from 'pg-cache';
|
|
4
|
+
import { createPostGraphileSchema } from 'postgraphile';
|
|
5
|
+
import * as http from 'node:http';
|
|
6
|
+
import * as https from 'node:https';
|
|
7
|
+
// Build GraphQL Schema SDL directly from Postgres using PostGraphile, without HTTP.
|
|
8
|
+
export async function buildSchemaSDL(opts) {
|
|
9
|
+
const database = opts.database ?? 'constructive';
|
|
10
|
+
const schemas = Array.isArray(opts.schemas) ? opts.schemas : [];
|
|
11
|
+
const settings = getGraphileSettings({
|
|
12
|
+
graphile: {
|
|
13
|
+
schema: schemas,
|
|
14
|
+
...(opts.graphile ?? {})
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
const pgPool = getPgPool({ database });
|
|
18
|
+
const schema = await createPostGraphileSchema(pgPool, schemas, settings);
|
|
19
|
+
return printSchema(schema);
|
|
20
|
+
}
|
|
21
|
+
// Fetch GraphQL Schema SDL from a running GraphQL endpoint via introspection.
|
|
22
|
+
// This centralizes GraphQL client usage in the server package to avoid duplicating deps in the CLI.
|
|
23
|
+
export async function fetchEndpointSchemaSDL(endpoint, opts) {
|
|
24
|
+
const url = new URL(endpoint);
|
|
25
|
+
const requestUrl = url;
|
|
26
|
+
const introspectionQuery = getIntrospectionQuery({ descriptions: true });
|
|
27
|
+
const postData = JSON.stringify({
|
|
28
|
+
query: introspectionQuery,
|
|
29
|
+
variables: null,
|
|
30
|
+
operationName: 'IntrospectionQuery',
|
|
31
|
+
});
|
|
32
|
+
const headers = {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
'Content-Length': String(Buffer.byteLength(postData)),
|
|
35
|
+
};
|
|
36
|
+
if (opts?.headerHost) {
|
|
37
|
+
headers['Host'] = opts.headerHost;
|
|
38
|
+
}
|
|
39
|
+
if (opts?.auth) {
|
|
40
|
+
headers['Authorization'] = opts.auth;
|
|
41
|
+
}
|
|
42
|
+
if (opts?.headers) {
|
|
43
|
+
for (const [key, value] of Object.entries(opts.headers)) {
|
|
44
|
+
headers[key] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const isHttps = requestUrl.protocol === 'https:';
|
|
48
|
+
const lib = isHttps ? https : http;
|
|
49
|
+
const responseData = await new Promise((resolve, reject) => {
|
|
50
|
+
const req = lib.request({
|
|
51
|
+
hostname: requestUrl.hostname,
|
|
52
|
+
port: (requestUrl.port ? Number(requestUrl.port) : (isHttps ? 443 : 80)),
|
|
53
|
+
path: requestUrl.pathname,
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers,
|
|
56
|
+
}, (res) => {
|
|
57
|
+
let data = '';
|
|
58
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
59
|
+
res.on('end', () => {
|
|
60
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
61
|
+
reject(new Error(`HTTP ${res.statusCode} – ${data}`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve(data);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
req.on('error', (err) => reject(err));
|
|
68
|
+
req.write(postData);
|
|
69
|
+
req.end();
|
|
70
|
+
});
|
|
71
|
+
let json;
|
|
72
|
+
try {
|
|
73
|
+
json = JSON.parse(responseData);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
throw new Error(`Failed to parse response: ${responseData}`);
|
|
77
|
+
}
|
|
78
|
+
if (json.errors) {
|
|
79
|
+
throw new Error('Introspection returned errors');
|
|
80
|
+
}
|
|
81
|
+
if (!json.data) {
|
|
82
|
+
throw new Error('No data in introspection response');
|
|
83
|
+
}
|
|
84
|
+
const schema = buildClientSchema(json.data);
|
|
85
|
+
return printSchema(schema);
|
|
86
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Minimal script to create a bucket in MinIO using @constructive-io/s3-utils
|
|
2
|
+
// Avoid strict type coupling between different @aws-sdk/client-s3 versions
|
|
3
|
+
// Loads graphql/server/.env by default when running via ts-node from this workspace
|
|
4
|
+
import 'dotenv/config';
|
|
5
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
6
|
+
import { createS3Bucket } from '@constructive-io/s3-utils';
|
|
7
|
+
const BUCKET = process.env.BUCKET_NAME || 'test-bucket';
|
|
8
|
+
const REGION = process.env.AWS_REGION || 'us-east-1';
|
|
9
|
+
const ACCESS_KEY = process.env.AWS_ACCESS_KEY || process.env.AWS_ACCESS_KEY_ID || 'minioadmin';
|
|
10
|
+
const SECRET_KEY = process.env.AWS_SECRET_KEY ||
|
|
11
|
+
process.env.AWS_SECRET_ACCESS_KEY ||
|
|
12
|
+
'minioadmin';
|
|
13
|
+
const ENDPOINT = process.env.MINIO_ENDPOINT || 'http://localhost:9000';
|
|
14
|
+
(async () => {
|
|
15
|
+
try {
|
|
16
|
+
const client = new S3Client({
|
|
17
|
+
region: REGION,
|
|
18
|
+
credentials: { accessKeyId: ACCESS_KEY, secretAccessKey: SECRET_KEY },
|
|
19
|
+
endpoint: ENDPOINT,
|
|
20
|
+
forcePathStyle: true,
|
|
21
|
+
});
|
|
22
|
+
// Hint downstream to apply MinIO policies
|
|
23
|
+
process.env.IS_MINIO = 'true';
|
|
24
|
+
const res = await createS3Bucket(client, BUCKET);
|
|
25
|
+
console.log(`[create-bucket] ${BUCKET}:`, res);
|
|
26
|
+
client.destroy();
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
console.error('[create-bucket] error', e);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
}
|
|
32
|
+
})();
|
package/esm/server.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { getEnvOptions } from '@constructive-io/graphql-env';
|
|
3
|
+
import { Logger } from '@pgpmjs/logger';
|
|
4
|
+
import { healthz, poweredBy, trustProxy } from '@pgpmjs/server-utils';
|
|
5
|
+
import { middleware as parseDomains } from '@constructive-io/url-domains';
|
|
6
|
+
import express from 'express';
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
import graphqlUpload from 'graphql-upload';
|
|
9
|
+
import { getPgPool } from 'pg-cache';
|
|
10
|
+
import requestIp from 'request-ip';
|
|
11
|
+
import { createApiMiddleware } from './middleware/api';
|
|
12
|
+
import { createAuthenticateMiddleware } from './middleware/auth';
|
|
13
|
+
import { cors } from './middleware/cors';
|
|
14
|
+
import { flush, flushService } from './middleware/flush';
|
|
15
|
+
import { graphile } from './middleware/graphile';
|
|
16
|
+
const log = new Logger('server');
|
|
17
|
+
export const GraphQLServer = (rawOpts = {}) => {
|
|
18
|
+
const envOptions = getEnvOptions(rawOpts);
|
|
19
|
+
const app = new Server(envOptions);
|
|
20
|
+
app.addEventListener();
|
|
21
|
+
app.listen();
|
|
22
|
+
};
|
|
23
|
+
class Server {
|
|
24
|
+
app;
|
|
25
|
+
opts;
|
|
26
|
+
constructor(opts) {
|
|
27
|
+
this.opts = getEnvOptions(opts);
|
|
28
|
+
const app = express();
|
|
29
|
+
const api = createApiMiddleware(opts);
|
|
30
|
+
const authenticate = createAuthenticateMiddleware(opts);
|
|
31
|
+
healthz(app);
|
|
32
|
+
trustProxy(app, opts.server.trustProxy);
|
|
33
|
+
// Warn if a global CORS override is set in production
|
|
34
|
+
const fallbackOrigin = opts.server?.origin?.trim();
|
|
35
|
+
if (fallbackOrigin && process.env.NODE_ENV === 'production') {
|
|
36
|
+
if (fallbackOrigin === '*') {
|
|
37
|
+
log.warn('CORS wildcard ("*") is enabled in production; this effectively disables CORS and is not recommended. Prefer per-API CORS via meta schema.');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
log.warn(`CORS override origin set to ${fallbackOrigin} in production. Prefer per-API CORS via meta schema.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
app.use(poweredBy('constructive'));
|
|
44
|
+
app.use(cors(fallbackOrigin));
|
|
45
|
+
app.use(graphqlUpload.graphqlUploadExpress());
|
|
46
|
+
app.use(parseDomains());
|
|
47
|
+
app.use(requestIp.mw());
|
|
48
|
+
app.use(api);
|
|
49
|
+
app.use(authenticate);
|
|
50
|
+
app.use(graphile(opts));
|
|
51
|
+
app.use(flush);
|
|
52
|
+
this.app = app;
|
|
53
|
+
}
|
|
54
|
+
listen() {
|
|
55
|
+
const { server } = this.opts;
|
|
56
|
+
this.app.listen(server?.port, server?.host, () => log.info(`listening at http://${server?.host}:${server?.port}`));
|
|
57
|
+
}
|
|
58
|
+
async flush(databaseId) {
|
|
59
|
+
await flushService(this.opts, databaseId);
|
|
60
|
+
}
|
|
61
|
+
getPool() {
|
|
62
|
+
return getPgPool(this.opts.pg);
|
|
63
|
+
}
|
|
64
|
+
addEventListener() {
|
|
65
|
+
const pgPool = this.getPool();
|
|
66
|
+
pgPool.connect(this.listenForChanges.bind(this));
|
|
67
|
+
}
|
|
68
|
+
listenForChanges(err, client, release) {
|
|
69
|
+
if (err) {
|
|
70
|
+
this.error('Error connecting with notify listener', err);
|
|
71
|
+
setTimeout(() => this.addEventListener(), 5000);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
client.on('notification', ({ channel, payload }) => {
|
|
75
|
+
if (channel === 'schema:update' && payload) {
|
|
76
|
+
log.info('schema:update', payload);
|
|
77
|
+
this.flush(payload);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
client.query('LISTEN "schema:update"');
|
|
81
|
+
client.on('error', (e) => {
|
|
82
|
+
this.error('Error with database notify listener', e);
|
|
83
|
+
release();
|
|
84
|
+
this.addEventListener();
|
|
85
|
+
});
|
|
86
|
+
this.log('connected and listening for changes...');
|
|
87
|
+
}
|
|
88
|
+
log(text) {
|
|
89
|
+
log.info(text);
|
|
90
|
+
}
|
|
91
|
+
error(text, err) {
|
|
92
|
+
log.error(text, err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export { Server };
|
package/esm/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/index.d.ts
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./server"), exports);
|
|
18
|
+
__exportStar(require("./schema"), exports);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PgpmOptions } from '@pgpmjs/types';
|
|
2
|
+
import { NextFunction, Request, Response } from 'express';
|
|
3
|
+
import './types';
|
|
4
|
+
export declare const getSubdomain: (reqDomains: string[]) => string | null;
|
|
5
|
+
export declare const createApiMiddleware: (opts: any) => (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
6
|
+
export declare const getApiConfig: (opts: PgpmOptions, req: Request) => Promise<any>;
|