@adobe/aio-cli-plugin-api-mesh 3.0.0 → 3.1.0-beta.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/src/server.js ADDED
@@ -0,0 +1,168 @@
1
+ // Resolve the path to 'fastify' and 'graphql-yoga' within the local 'node_modules'
2
+ const fastifyPath = require.resolve('fastify', { paths: [process.cwd()] });
3
+ const yogaPath = require.resolve('graphql-yoga', { paths: [process.cwd()] });
4
+
5
+ // Load 'fastify' and 'graphql-yoga' using the resolved paths
6
+ const fastify = require(fastifyPath);
7
+ const { createYoga } = require(yogaPath);
8
+ const logger = require('./classes/logger');
9
+
10
+ //Load the functions from serverUtils.js
11
+ const {
12
+ readMeshConfig,
13
+ removeRequestHeaders,
14
+ prepSourceResponseHeaders,
15
+ processResponseHeaders,
16
+ } = require('./serverUtils');
17
+
18
+ let yogaServer = null;
19
+ let meshConfig;
20
+
21
+ // catch unhandled promise rejections
22
+ process.on('unhandledRejection', reason => {
23
+ logger.error('Unhandled Rejection at:', reason.stack || reason);
24
+ });
25
+
26
+ // catch uncaught exceptions
27
+ process.on('uncaughtException', err => {
28
+ logger.error('Uncaught Exception thrown');
29
+ logger.error(err.stack);
30
+ process.exit(1);
31
+ });
32
+
33
+ // get meshId from command line arguments
34
+ const meshId = process.argv[2];
35
+
36
+ // get PORT number from command line arguments
37
+ const portNo = parseInt(process.argv[3]);
38
+
39
+ const getCORSOptions = () => {
40
+ try {
41
+ const currentWorkingDirectory = process.cwd();
42
+ const meshConfigPath = `${currentWorkingDirectory}/mesh-artifact/${meshId}/.meshrc.json`;
43
+
44
+ const meshConfig = require(meshConfigPath);
45
+ const { responseConfig } = meshConfig;
46
+ const { CORS } = responseConfig;
47
+
48
+ return CORS;
49
+ } catch (e) {
50
+ return {};
51
+ }
52
+ };
53
+
54
+ const getYogaServer = async () => {
55
+ if (yogaServer) {
56
+ return yogaServer;
57
+ } else {
58
+ const currentWorkingDirectory = process.cwd();
59
+ const meshArtifactsPath = `${currentWorkingDirectory}/mesh-artifact/${meshId}`;
60
+
61
+ const meshArtifacts = require(meshArtifactsPath);
62
+ const { getBuiltMesh } = meshArtifacts;
63
+
64
+ const tenantMesh = await getBuiltMesh();
65
+ const corsOptions = getCORSOptions();
66
+
67
+ logger.info('Creating graphQL server');
68
+
69
+ meshConfig = readMeshConfig(meshId);
70
+
71
+ yogaServer = createYoga({
72
+ plugins: tenantMesh.plugins,
73
+ graphqlEndpoint: `/graphql`,
74
+ graphiql: true,
75
+ cors: corsOptions,
76
+ });
77
+
78
+ return yogaServer;
79
+ }
80
+ };
81
+
82
+ const app = fastify();
83
+
84
+ app.route({
85
+ method: ['GET', 'POST'],
86
+ url: '/graphql',
87
+ handler: async (req, res) => {
88
+ logger.info('Request received: ', req.body);
89
+
90
+ let body = null;
91
+ let responseBody = null;
92
+ let includeMetaData = false;
93
+
94
+ if (req.headers['x-include-metadata'] && req.headers['x-include-metadata'].length > 0) {
95
+ if (req.headers['x-include-metadata'].toLowerCase() === 'true') {
96
+ includeMetaData = true;
97
+ }
98
+ }
99
+
100
+ const response = await yogaServer.handleNodeRequest(req, {
101
+ req,
102
+ reply: res,
103
+ });
104
+
105
+ try {
106
+ try {
107
+ body = await response.text();
108
+ if (body) {
109
+ responseBody = JSON.parse(body);
110
+ }
111
+ } catch (err) {
112
+ logger.error(`Error parsing response body: ${err}`);
113
+ logger.error(response);
114
+ throw new Error(`Error parsing response body: ${err}`);
115
+ }
116
+ //Set the value of includeHTTPDetails flag
117
+
118
+ const includeHTTPDetails = !!meshConfig?.responseConfig?.includeHTTPDetails;
119
+ const meshHTTPDetails = responseBody?.extensions?.httpDetails;
120
+ logger.info('Mesh HTTP Details: ', meshHTTPDetails);
121
+
122
+ /* the logic for handling mesh response headers using includeMetaData */
123
+ prepSourceResponseHeaders(meshHTTPDetails, req.id);
124
+ const responseHeaders = processResponseHeaders(meshId, req.id, includeMetaData, req.method);
125
+
126
+ /** Adding the yoga response headers to the response */
127
+ response.headers?.forEach((value, key) => {
128
+ res.header(key, value);
129
+ });
130
+
131
+ // Delete the httpDetails extensions details if mesh owner has disabled those details in the config
132
+ if (includeHTTPDetails !== true) {
133
+ delete responseBody?.extensions?.httpDetails;
134
+ }
135
+
136
+ //make sure to remove the request headers from cache after the request is complete
137
+ removeRequestHeaders(req.id);
138
+ const fastifyResponseBody = JSON.stringify(responseBody);
139
+ res.status(response.status).headers(responseHeaders).send(fastifyResponseBody);
140
+ } catch (err) {
141
+ logger.error(`Error parsing response body: ${err}`);
142
+ //we have this fallback catch clause if someone wants to load the graphiql engine. This returns the default headers back
143
+ response.headers?.forEach((value, key) => {
144
+ res.header(key, value);
145
+ });
146
+ res.status(response.status);
147
+ res.send(response.body);
148
+ }
149
+
150
+ return res;
151
+ },
152
+ });
153
+
154
+ app.listen(
155
+ {
156
+ //set the port no of the server based on the input value
157
+ port: portNo,
158
+ },
159
+ async err => {
160
+ if (err) {
161
+ console.error(err);
162
+ process.exit(1);
163
+ }
164
+ yogaServer = await getYogaServer();
165
+
166
+ console.log(`Server is running on http://localhost:${portNo}/graphql`);
167
+ },
168
+ );
@@ -0,0 +1,323 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const LRUCache = require('lru-cache');
4
+ const logger = require('./classes/logger');
5
+
6
+ const headersCache = new LRUCache({
7
+ max: parseInt(process.env.CACHE_OPT_MAX || '500', 10),
8
+ ttl: parseInt(process.env.CACHE_HEADERS_TTL || '300000', 10), // 5 mins
9
+ dispose: (value, key, reason) => {
10
+ logger.info(`Removing headers for ${key} from headers cache because ${reason}`);
11
+ },
12
+ });
13
+
14
+ /**
15
+ * Read .meshrc.json file stored in the mesh-artifact for a particular mesh
16
+ * Parse the file and store into meshConfig object
17
+ * @param appRootDir
18
+ * @param meshId
19
+ */
20
+ function readMeshConfig(meshId) {
21
+ const configPath = path.resolve(process.cwd(), 'mesh-artifact', `${meshId}`, '.meshrc.json');
22
+ if (fs.existsSync(configPath)) {
23
+ const meshrcFile = fs.readFileSync(configPath).toString();
24
+
25
+ return JSON.parse(meshrcFile);
26
+ }
27
+ }
28
+
29
+ function removeRequestHeaders(requesId) {
30
+ headersCache.delete(requesId);
31
+ }
32
+
33
+ /**
34
+ * This function loops through the mesh http details plugin and saves the response headers into an LRU cache
35
+ * @param meshHTTPDetails this is the object coming from the graphql-mesh plugin that contains information about the different sources
36
+ * @param requestId
37
+ */
38
+ function prepSourceResponseHeaders(meshHTTPDetails, requestId) {
39
+ const mappedResponseHeaders = [];
40
+ //const requestId = request_context_1.requestContext.get('requestId');
41
+ meshHTTPDetails?.forEach(element => {
42
+ const headers = Object.entries(element.response.headers);
43
+ headers?.forEach(value => {
44
+ const header = value[0];
45
+ const headerValue = value[1];
46
+ const sourceName = element.sourceName;
47
+ const url = element.request.url;
48
+ const mappedResponseHeader = {
49
+ name: header.toLowerCase(),
50
+ source: sourceName,
51
+ values: [headerValue],
52
+ };
53
+ const sourceMarkedHeader = {
54
+ name:
55
+ //we want to return all the original headers from magento and source prefixed for everyone else
56
+ header.toLowerCase() !== 'cache-control' && !url.toLowerCase().includes('magento')
57
+ ? `x-${element.sourceName}-${header.toLowerCase()}`
58
+ : header.toLowerCase(),
59
+ source: sourceName,
60
+ values: [headerValue],
61
+ };
62
+ mappedResponseHeaders.push(sourceMarkedHeader);
63
+ mappedResponseHeaders.push(mappedResponseHeader);
64
+ });
65
+ //Update the headers LRU cache
66
+ setSourceResponseHeaders(requestId, mappedResponseHeaders);
67
+ });
68
+ }
69
+
70
+ function setSourceResponseHeaders(requestId, responseHeaders) {
71
+ if (headersCache.has(requestId)) {
72
+ const currentResponseHeaders = headersCache.get(requestId);
73
+ const concatResponseHeaders = currentResponseHeaders.concat(responseHeaders);
74
+ headersCache.set(requestId, concatResponseHeaders);
75
+ } else {
76
+ headersCache.set(requestId, responseHeaders);
77
+ }
78
+ }
79
+
80
+ /**
81
+ *
82
+ * @param meshId
83
+ * @param requestId
84
+ * @param includeMetadata
85
+ * @param method
86
+ * @returns {Object.<string, string|string[]>}
87
+ */
88
+ function processResponseHeaders(meshId, requestId, includeMetadata, method) {
89
+ const meshResponseConfig = getMeshResponseConfig(meshId);
90
+ const responseHeaders = headersCache.get(requestId);
91
+ const sourceResponseConfig = getSourceResponseHeaders(
92
+ responseHeaders,
93
+ meshId,
94
+ requestId,
95
+ includeMetadata,
96
+ );
97
+ return processMeshResponseHeaders(
98
+ meshResponseConfig,
99
+ sourceResponseConfig,
100
+ method,
101
+ responseHeaders,
102
+ );
103
+ }
104
+
105
+ function getMeshResponseConfig(meshId) {
106
+ const meshConfig = readMeshConfig(meshId);
107
+ return meshConfig?.responseConfig;
108
+ }
109
+
110
+ function getSourceResponseHeaders(responseHeaders, meshId, requestId, includeMetadata) {
111
+ const sourceResponseHeaders = {};
112
+ const sourceResponseHeadersMap = new Map();
113
+
114
+ const currentWorkingDirectory = process.cwd();
115
+ const meshConfigPath = `${currentWorkingDirectory}/mesh-artifact/${meshId}/.meshrc.json`;
116
+
117
+ const meshConfig = require(meshConfigPath);
118
+ if (meshConfig) {
119
+ meshConfig.sources.forEach(source => {
120
+ if (source.responseConfig && source.responseConfig.headers) {
121
+ sourceResponseHeadersMap.set(source.name, source.responseConfig.headers);
122
+ }
123
+ });
124
+ responseHeaders?.forEach(element => {
125
+ const respArray = sourceResponseHeadersMap.get(element.source);
126
+ const lower = respArray?.map(element => {
127
+ return element.toLowerCase();
128
+ });
129
+ if (lower && lower.includes(element.name)) {
130
+ sourceResponseHeaders[element.name] = element.values;
131
+ }
132
+ if (includeMetadata) {
133
+ sourceResponseHeaders[element.name] = element.values;
134
+ }
135
+ });
136
+ } else {
137
+ logger.error(`No meshid ${meshId} found for requestId: ${requestId}`);
138
+ throw new Error(`No meshid ${meshId} found`, 'getSourceResponseHeaders', requestId);
139
+ }
140
+ // }
141
+ return sourceResponseHeaders || {};
142
+ }
143
+
144
+ function processMeshResponseHeaders(
145
+ meshResponseConfig,
146
+ sourceResponseConfig,
147
+ method,
148
+ responseHeaders,
149
+ ) {
150
+ //Since we do not want any caching to happen on posts we need to make sure any cache-control headers
151
+ //are removed on posts. This includes at the mesh and source level
152
+ if (method.toLowerCase() === 'post') {
153
+ const removedCacheHeadersConfig = Object.fromEntries(
154
+ Object.entries(sourceResponseConfig).map(([k, v]) => [k.toLowerCase(), v]),
155
+ );
156
+ delete removedCacheHeadersConfig['cache-control'];
157
+ //if there are mesh headers we want to remove them as well
158
+ if (meshResponseConfig && meshResponseConfig.headers) {
159
+ const meshHeaders = Object.fromEntries(
160
+ Object.entries(meshResponseConfig.headers).map(([k, v]) => [
161
+ k.toLowerCase(),
162
+ v.toLowerCase(),
163
+ ]),
164
+ );
165
+ delete meshHeaders['cache-control'];
166
+ return { ...meshHeaders } || {};
167
+ }
168
+ return { ...removedCacheHeadersConfig } || {};
169
+ }
170
+ //All other requests go through the usual path
171
+ if (meshResponseConfig && meshResponseConfig.headers) {
172
+ //make sure we are standardizing all the headers
173
+ const meshHeaders = Object.fromEntries(
174
+ Object.entries(meshResponseConfig.headers).map(([k, v]) => [
175
+ k.toLowerCase(),
176
+ v.toLowerCase(),
177
+ ]),
178
+ );
179
+ return { ...meshHeaders, ...sourceResponseConfig } || {};
180
+ } else {
181
+ if (responseHeaders) {
182
+ const ccDirectives = getCacheControlDirectives(responseHeaders);
183
+ return { ...sourceResponseConfig, ...ccDirectives } || {};
184
+ }
185
+ }
186
+ return { ...sourceResponseConfig } || {};
187
+ }
188
+
189
+ /**
190
+ * Returns the lowest common denominator on all the sources cache-control headers
191
+ * @param responseHeaders
192
+ * @returns
193
+ */
194
+ function getCacheControlDirectives(responseHeaders) {
195
+ let ccDirectives = {};
196
+ responseHeaders?.forEach(element => {
197
+ if (element.name.toLowerCase() === 'cache-control') {
198
+ const currentCacheMap = parseCacheControl(element.values.toString());
199
+ const standardDizedCacheMap = Object.fromEntries(
200
+ Object.entries(currentCacheMap).map(([k, v]) => [k.toLowerCase(), v.toLowerCase()]),
201
+ );
202
+ ccDirectives = resolveCacheDirectives(ccDirectives, standardDizedCacheMap);
203
+ }
204
+ });
205
+ return { 'cache-control': ccDirectivesToString(ccDirectives) };
206
+ }
207
+
208
+ /**
209
+ * Parses out the cache-control headers into a map
210
+ * @param directives
211
+ * @returns
212
+ */
213
+ function parseCacheControl(directives) {
214
+ // 1: directive = 2: token 3: quoted-string
215
+ // eslint-disable-next-line
216
+ const regex = /(?:^|(?:\s*\,\s*))([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)(?:\=(?:([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)|(?:\"((?:[^"\\]|\\.)*)\")))?/g;
217
+ const header = {};
218
+ const err = directives.replace(regex, function ($0, $1, $2, $3) {
219
+ const value = $2 || $3;
220
+ header[$1] = value ? value.toLowerCase() : $1;
221
+ return '';
222
+ });
223
+ return err ? {} : header;
224
+ }
225
+
226
+ /**
227
+ * Runs a lowest common denominator algorithm on the current source cache-control header
228
+ * @param lowestValuesCacheDirectives - object containing the lowest values of the cache-control headers
229
+ * @param currentDirectives - current source cache-control headers
230
+ * @returns lowestValuesCacheDirectives
231
+ */
232
+ function resolveCacheDirectives(lowestValuesCacheDirectives, currentDirectives) {
233
+ //if any header contains no-store, we are done
234
+ if (lowestValuesCacheDirectives['no-store']) {
235
+ return lowestValuesCacheDirectives;
236
+ }
237
+ if (currentDirectives['no-store']) {
238
+ lowestValuesCacheDirectives = {};
239
+ lowestValuesCacheDirectives['no-store'] = 'no-store';
240
+ return lowestValuesCacheDirectives;
241
+ }
242
+ //id min values for each of these directives
243
+ const minDirectives = [
244
+ 'min-fresh',
245
+ 'max-age',
246
+ 'max-stale',
247
+ 's-maxage',
248
+ 'stale-if-error',
249
+ 'stale-while-revalidate',
250
+ ];
251
+ minDirectives.forEach(element => {
252
+ updateToMin(element, currentDirectives[element], lowestValuesCacheDirectives);
253
+ });
254
+ //add these directives, if they are not already present
255
+ const otherDirectives = [
256
+ 'public',
257
+ 'private',
258
+ 'immutable',
259
+ 'no-cache',
260
+ 'no-transform',
261
+ 'must-revalidate',
262
+ 'proxy-revalidate',
263
+ 'must-understand',
264
+ ];
265
+ Object.keys(currentDirectives).forEach(key => {
266
+ if (otherDirectives.includes(key) && !lowestValuesCacheDirectives[key]) {
267
+ lowestValuesCacheDirectives[key] = currentDirectives[key];
268
+ }
269
+ });
270
+ return lowestValuesCacheDirectives;
271
+ }
272
+
273
+ /**
274
+ * Updates a cache-control header value to the lowest value if required
275
+ * @param key
276
+ * @param candidateMin
277
+ * @param cachedHeaders
278
+ * @returns
279
+ */
280
+ function updateToMin(key, candidateMin, cachedHeaders) {
281
+ //first check if both values exist and are not undefined
282
+ if (cachedHeaders[key] && candidateMin) {
283
+ //if the value to be replaced is not a number and the candidate is a number we do a direct replacement
284
+ if (isNaN(Number(cachedHeaders[key])) && !isNaN(Number(candidateMin))) {
285
+ cachedHeaders[key] = candidateMin;
286
+ }
287
+ //if both values are integers and the candidate is lower than the existing lowest value, replace the current value with the candidate
288
+ else if (!isNaN(Number(cachedHeaders[key])) && !isNaN(Number(candidateMin))) {
289
+ if (Number(cachedHeaders[key]) > Number(candidateMin)) {
290
+ cachedHeaders[key] = candidateMin;
291
+ }
292
+ }
293
+ }
294
+ //do a direct in-place update of the existing array
295
+ else if (candidateMin) {
296
+ cachedHeaders[key] = candidateMin;
297
+ }
298
+ return cachedHeaders;
299
+ }
300
+
301
+ /**
302
+ * Returns the cache-control headers as a string
303
+ * @param directives
304
+ * @returns
305
+ */
306
+ function ccDirectivesToString(directives) {
307
+ const chStr = [];
308
+ Object.keys(directives).forEach(key => {
309
+ if (directives[key] === key) {
310
+ chStr.push(key);
311
+ } else {
312
+ chStr.push(key + '=' + directives[key]);
313
+ }
314
+ });
315
+ return chStr.toString();
316
+ }
317
+
318
+ module.exports = {
319
+ readMeshConfig,
320
+ removeRequestHeaders,
321
+ prepSourceResponseHeaders,
322
+ processResponseHeaders,
323
+ };
@@ -5,25 +5,35 @@
5
5
  "access": "public"
6
6
  },
7
7
  "dependencies": {
8
- "@graphql-mesh/cli": "^0.78.33",
9
- "@graphql-mesh/graphql": "^0.32.4",
10
- "@graphql-mesh/json-schema": "^0.35.28",
11
- "@graphql-mesh/openapi": "^0.33.39",
12
- "@graphql-mesh/plugin-http-details-extensions": "^0.0.12",
13
- "@graphql-mesh/runtime": "^0.44.37",
14
- "@graphql-mesh/transform-encapsulate": "^0.3.114",
15
- "@graphql-mesh/transform-federation": "^0.9.60",
16
- "@graphql-mesh/transform-filter-schema": "^0.14.113",
17
- "@graphql-mesh/transform-hoist-field": "^0.1.78",
18
- "@graphql-mesh/transform-naming-convention": "^0.12.3",
19
- "@graphql-mesh/transform-prefix": "^0.11.104",
20
- "@graphql-mesh/transform-prune": "^0.0.88",
21
- "@graphql-mesh/transform-rename": "^0.13.2",
22
- "@graphql-mesh/transform-replace-field": "^0.3.112",
23
- "@graphql-mesh/transform-resolvers-composition": "^0.12.111",
24
- "@graphql-mesh/transform-type-merging": "^0.4.56",
25
- "@graphql-mesh/types": "^0.87.1",
26
- "graphql": "^16.6.0"
8
+ "@graphql-mesh/cli": "0.82.30",
9
+ "@graphql-mesh/graphql": "0.34.13",
10
+ "@graphql-mesh/json-schema": "0.35.38",
11
+ "@graphql-mesh/openapi": "0.33.39",
12
+ "@graphql-mesh/plugin-http-details-extensions": "0.1.21",
13
+ "@graphql-mesh/runtime": "0.46.21",
14
+ "@graphql-mesh/transform-encapsulate": "0.4.21",
15
+ "@graphql-mesh/transform-federation": "0.11.14",
16
+ "@graphql-mesh/transform-filter-schema": "0.15.23",
17
+ "@graphql-mesh/transform-hoist-field": "0.2.21",
18
+ "@graphql-mesh/transform-naming-convention": "0.13.22",
19
+ "@graphql-mesh/transform-prefix": "0.12.22",
20
+ "@graphql-mesh/transform-prune": "0.1.20",
21
+ "@graphql-mesh/transform-rename": "0.14.22",
22
+ "@graphql-mesh/transform-replace-field": "0.4.20",
23
+ "@graphql-mesh/transform-resolvers-composition": "0.13.20",
24
+ "@graphql-mesh/transform-type-merging": "0.5.20",
25
+ "@graphql-mesh/types": "0.91.12",
26
+ "@graphql-mesh/soap": "0.14.25",
27
+ "@graphql-mesh/http": "^0.96.9",
28
+ "graphql": "^16.6.0",
29
+ "@adobe/plugin-hooks": "^0.1.0",
30
+ "@adobe/plugin-on-fetch": "^0.1.0",
31
+ "eslint-plugin-no-loops": "^0.3.0",
32
+ "eslint-plugin-node": "^11.1.0",
33
+ "eslint-plugin-security": "^1.5.0",
34
+ "eslint-plugin-sonarjs": "^0.16.0",
35
+ "fastify": "^4.10.2",
36
+ "graphql-yoga": "3.1.1"
27
37
  },
28
38
  "engines": {
29
39
  "node": ">=18.0.0"
package/src/utils.js CHANGED
@@ -41,6 +41,16 @@ const envFileFlag = Flags.string({
41
41
  default: '.env',
42
42
  });
43
43
 
44
+ const portNoFlag = Flags.integer({
45
+ char: 'p',
46
+ description: 'Port number for the local dev server',
47
+ });
48
+
49
+ const debugFlag = Flags.boolean({
50
+ description: 'Enable debugging mode',
51
+ default: false,
52
+ });
53
+
44
54
  /**
45
55
  * Parse the meshConfig and get the list of (local) files to be imported
46
56
  *
@@ -387,4 +397,6 @@ module.exports = {
387
397
  validateEnvFileFormat,
388
398
  validateAndInterpolateMesh,
389
399
  getAppRootDir,
400
+ portNoFlag,
401
+ debugFlag,
390
402
  };
package/src/uuid.js ADDED
@@ -0,0 +1,21 @@
1
+ const { v4: uuidv4 } = require('uuid');
2
+
3
+ /**
4
+ * Class representing a UUID object which is used as a param across the application
5
+ */
6
+ class UUID {
7
+ constructor(str) {
8
+ this.m_str = str || UUID.newUuid().toString();
9
+ }
10
+
11
+ toString() {
12
+ return this.m_str;
13
+ }
14
+
15
+ static newUuid() {
16
+ const uuid = uuidv4();
17
+ return new UUID(uuid);
18
+ }
19
+ }
20
+
21
+ module.exports = UUID;