Skip to content
This repository was archived by the owner on Mar 17, 2023. It is now read-only.

Commit 9b71a34

Browse files
committed
add graphql
1 parent 3135b1b commit 9b71a34

File tree

10 files changed

+816
-9
lines changed

10 files changed

+816
-9
lines changed

README.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,24 @@ then you can use this so called "sdk".
2020
```javascript
2121
const sdk = require('sdk');
2222

23-
2423
```
2524

25+
26+
### Configurations
27+
28+
Most of commont configuration may be added via environment variables. (with prefix `SDK_`).
29+
This way, the app can be changed without touching the code.
30+
31+
For example, you develop it in redis stream, zipkin memory, cache memory.
32+
But in the server you use kafka stream, zipkin http, and cache with redis. Just use `SDK_` prefix env.
33+
34+
2635
## Integration
2736

2837
This curated list is not really binding into a Framework.
2938
Please feel free to use any framework to use. \*as long as its compatible :-)
3039

40+
3141
## Utilities
3242

3343
### Cache
@@ -75,14 +85,50 @@ Bunyan ringbuffer is enabled by default. So we can use to track logs, even in un
7585
try to get ringBuffer object? this how we do that: `sdk.log.ringBuffer`.
7686

7787

78-
### Metrics
88+
## Metrics
89+
90+
### Zipkin
91+
92+
For easier tracer, we can use zipkin as tracer exporter.
93+
Just enable it with config `ZIPKIN_ENABLE` or via env `SDK_ZIPKIN_ENABLE` set to `true`.
7994

95+
Env Variables example:
96+
97+
```
98+
SDK_ZIPKIN_ENABLE: true,
99+
SDK_ZIPKIN_DRIVER: 'http',
100+
SDK_ZIPKIN_HTTP_ENDPOINT: 'http://zipkin-service:9411/api/v2/spans',
101+
```
80102

81103
## Stream
82104

83105

84106
## Graphql
85107

108+
A binding for ApolloClient graphql.
109+
110+
> if you enable zipkin, then its zipkin wrapped also
111+
112+
**Example**:
113+
114+
```javascript
115+
await sdk.enable_graphql({
116+
uri: 'https://some-graphql-server.com/graphql',
117+
remoteServiceName: 'fakeql',
118+
});
119+
120+
const query = `
121+
query {
122+
user (id: 1) {
123+
id
124+
name
125+
phone
126+
}
127+
}
128+
`;
129+
130+
const response = await sdk.graphql.query({ query, throwError: true, withCache: false });
131+
```
86132

87133
## Authorization
88134

jest.setup.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ fetchMock.dontMock();
1616
// fake timer
1717
// jest.useFakeTimers();
1818

19-
// timeout 10s
20-
// jest.setTimeout(10000);
19+
// timeout 30s
20+
jest.setTimeout(30000);

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
]
3434
},
3535
"dependencies": {
36+
"apollo-cache-inmemory": "^1.6.5",
37+
"apollo-client": "^2.6.8",
38+
"apollo-link-http": "^1.5.17",
3639
"aws-sdk": "^2.669.0",
3740
"bluebird": "^3.7.2",
3841
"bunyan": "^1.8.12",
@@ -43,13 +46,17 @@
4346
"cache-manager": "^3.3.0",
4447
"cache-manager-redis-store": "^2.0.0",
4548
"connection-string": "^3.2.2",
49+
"crypto": "^1.0.1",
4650
"flaverr": "^1.10.0",
51+
"graphql": "^14.5.8",
52+
"graphql-tag": "^2.10.3",
4753
"lodash": "^4.17.15",
4854
"machine": "^15.2.2",
49-
"moment": "^2.25.3",
55+
"node-fetch": "^2.6.0",
5056
"rxjs": "^6.5.5",
5157
"uuid": "^8.0.0",
5258
"zipkin": "^0.21.0",
59+
"zipkin-instrumentation-fetch": "^0.21.0",
5360
"zipkin-transport-http": "^0.21.0"
5461
}
5562
}

src/graphql/connect.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
const { ApolloClient } = require('apollo-client');
2+
const { InMemoryCache } = require('apollo-cache-inmemory');
3+
const { HttpLink } = require('apollo-link-http');
4+
const wrapFetch = require('zipkin-instrumentation-fetch');
5+
const fetch = require('node-fetch');
6+
const crypto = require('crypto');
7+
const flaverr = require('flaverr');
8+
const _ = require('lodash');
9+
const qgl = require('graphql-tag');
10+
11+
const makeRequest = ({
12+
sdk, method, client, remoteServiceName, logger,
13+
}) => (configs) => {
14+
const { withCache = false, cacheOptions = {}, throwError = true } = configs;
15+
16+
const formatError = (err) => ({
17+
message: err.message,
18+
locations: [],
19+
path: [],
20+
});
21+
22+
// auto throw, if configs.throwError is true
23+
const hash = crypto
24+
.createHash('md5')
25+
.update(JSON.stringify(_.pick(configs, ['query', 'mutation', 'variables'])))
26+
.digest('hex')
27+
.substr(0, 14);
28+
29+
const log = logger.child({ hash, method });
30+
31+
const sendRequest = () => {
32+
log.trace('request sent!', configs);
33+
34+
// auto translate if gql is string
35+
if (configs.query && typeof configs.query === 'string') {
36+
configs.query = qgl(configs.query); // eslint-disable-line no-param-reassign
37+
}
38+
if (configs.mutation && typeof configs.mutation === 'string') {
39+
configs.mutation = qgl(configs.mutation); // eslint-disable-line no-param-reassign
40+
}
41+
42+
return client[method](configs)
43+
.then((response) => {
44+
if ((response.errors || []).length > 0 && throwError) {
45+
throw flaverr(
46+
_.pick(response.errors[0], ['code', 'httpStatusCode', 'locations', 'path']),
47+
new Error(response.errors[0].message),
48+
);
49+
}
50+
51+
log.trace('response success!');
52+
return ({
53+
status: 200,
54+
errors: response.errors || [],
55+
...response,
56+
hash,
57+
});
58+
})
59+
.catch((err) => {
60+
61+
if (throwError) {
62+
// if there is more information
63+
if (Array.isArray(err.graphQLErrors) && err.graphQLErrors.length > 0) {
64+
throw flaverr(
65+
_.pick(err.graphQLErrors[0], ['code', 'httpStatusCode', 'locations', 'path']),
66+
err,
67+
);
68+
}
69+
70+
// else just throw error
71+
throw err;
72+
}
73+
74+
if (err.networkError) {
75+
log.trace('failed! networkError', err);
76+
return {
77+
status: 500,
78+
data: {},
79+
errors: [formatError(new Error('Backend Error!')), formatError(err.networkError)],
80+
hash,
81+
};
82+
}
83+
84+
log.trace('failed! UnknownError', err);
85+
86+
const originalErrors = (Array.isArray(err.graphQLErrors))
87+
? err.graphQLErrors.map(formatError)
88+
: [formatError(err)];
89+
90+
return {
91+
status: 200,
92+
errors: [formatError(new Error('Unknown Error!')), ...originalErrors],
93+
data: null,
94+
hash,
95+
};
96+
});
97+
};
98+
99+
100+
if (withCache) {
101+
return sdk.cache.wrap(`${remoteServiceName}/${hash}`, () => sendRequest(), cacheOptions);
102+
}
103+
104+
return sendRequest();
105+
};
106+
107+
module.exports = async ({
108+
configs: {
109+
defaultOptions = {}, url, uri, remoteServiceName = 'unnamed-remote-service',
110+
},
111+
sdk,
112+
}) => {
113+
const logger = sdk.log.child({
114+
service: 'graphql',
115+
remoteServiceName,
116+
});
117+
118+
const endpoint = url || uri; // -_- which one is right?
119+
120+
const defaultOptionsOverride = {
121+
watchQuery: {
122+
fetchPolicy: 'no-cache',
123+
errorPolicy: 'ignore',
124+
},
125+
query: {
126+
fetchPolicy: 'no-cache',
127+
errorPolicy: 'all',
128+
},
129+
...defaultOptions,
130+
};
131+
132+
// default fetcher
133+
let fetcher = fetch;
134+
135+
// add zipkin tracer if enabled
136+
if (sdk.zipkinTracer) {
137+
fetcher = wrapFetch(fetch, {
138+
tracer: sdk.zipkinTracer,
139+
remoteServiceName,
140+
});
141+
}
142+
143+
const cache = new InMemoryCache();
144+
const link = new HttpLink({ fetch: fetcher, uri: endpoint });
145+
const client = new ApolloClient({ cache, link, defaultOptions: defaultOptionsOverride });
146+
147+
logger.info(`graphql: graphql enabled! ${remoteServiceName}`);
148+
149+
150+
return {
151+
query: makeRequest({
152+
method: 'query', sdk, client, remoteServiceName, logger,
153+
}),
154+
mutate: makeRequest({
155+
method: 'mutate', sdk, client, remoteServiceName, logger,
156+
}),
157+
client,
158+
};
159+
};

src/index.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const { parseConfig } = require('./utils/config');
99
// trackers
1010
const { createZipkinTracer } = require('./tracers/zipkin/index');
1111

12+
// connect
13+
const connectGraphql = require('./graphql/connect');
14+
1215
// default instance
1316
const defaultInstance = {
1417
configs: {},
@@ -31,13 +34,57 @@ const defaultInstance = {
3134
graphqls: { default: null },
3235
};
3336

37+
38+
// ---------------------------------------------------------------------------
39+
3440
// just a small decoration, also used for test binding :-)
3541
function logoPrint() {
3642
const logo = '==== SDK-NODEJS Bootstrap! ====';
3743
this.log.trace(logo);
3844
return logo;
3945
}
4046

47+
// ---------------------------------------------------------------------------
48+
49+
async function enableGraphql(name = 'default', configs = {}) {
50+
const sdk = this; // -_-
51+
52+
// direct config for default? ok then
53+
if (typeof name === 'object') {
54+
configs = name; // eslint-disable-line no-param-reassign
55+
name = 'default'; // eslint-disable-line no-param-reassign
56+
}
57+
58+
// connect and add to graphqls list
59+
const connection = await connectGraphql({
60+
configs: { remoteServiceName: name, ...configs },
61+
sdk,
62+
});
63+
sdk.graphqls[name] = connection;
64+
65+
// default? add to default
66+
if (name === 'default') {
67+
sdk.graphql = connection;
68+
}
69+
70+
return sdk.graphqls[name];
71+
}
72+
73+
async function disableGraphql(name = 'default') {
74+
const sdk = this; // -_-
75+
76+
// just delete instance
77+
delete sdk.graphqls[name];
78+
79+
// if default, then remove default
80+
if (name === 'default') {
81+
sdk.graphql = null;
82+
}
83+
}
84+
85+
// ---------------------------------------------------------------------------
86+
87+
4188
async function bootstrap(environment = 'production', configOverrides = {}, useEnv = true, overwriteGlobal = false) {
4289
// create new instance
4390
const instance = _.cloneDeep(defaultInstance);
@@ -58,6 +105,13 @@ async function bootstrap(environment = 'production', configOverrides = {}, useEn
58105
instance.zipkinTracer = await createZipkinTracer({ configs, sdk: instance });
59106
}
60107

108+
// integration with graphql, with back compatibility for camelcase
109+
instance.enable_graphql = enableGraphql.bind(instance);
110+
instance.disable_graphql = disableGraphql.bind(instance);
111+
instance.enableGraphql = enableGraphql.bind(instance);
112+
instance.disableGraphql = disableGraphql.bind(instance);
113+
114+
61115
// put it in global var for default usage later with getInstance()
62116
if (overwriteGlobal || !global.ncuratedSDK) {
63117
global.ncuratedSDK = instance;

0 commit comments

Comments
 (0)