@adobe/aio-cli-plugin-api-mesh 3.0.1 → 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/oclif.manifest.json +1 -1
- package/package.json +13 -5
- package/src/commands/api-mesh/__tests__/run.test.js +783 -0
- package/src/commands/api-mesh/init.js +8 -0
- package/src/commands/api-mesh/run.js +156 -0
- package/src/helpers.js +58 -5
- package/src/server.js +168 -0
- package/src/serverUtils.js +323 -0
- package/src/templates/package.json +29 -19
- package/src/utils.js +12 -0
- package/src/uuid.js +21 -0
|
@@ -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": "
|
|
9
|
-
"@graphql-mesh/graphql": "
|
|
10
|
-
"@graphql-mesh/json-schema": "
|
|
11
|
-
"@graphql-mesh/openapi": "
|
|
12
|
-
"@graphql-mesh/plugin-http-details-extensions": "
|
|
13
|
-
"@graphql-mesh/runtime": "
|
|
14
|
-
"@graphql-mesh/transform-encapsulate": "
|
|
15
|
-
"@graphql-mesh/transform-federation": "
|
|
16
|
-
"@graphql-mesh/transform-filter-schema": "
|
|
17
|
-
"@graphql-mesh/transform-hoist-field": "
|
|
18
|
-
"@graphql-mesh/transform-naming-convention": "
|
|
19
|
-
"@graphql-mesh/transform-prefix": "
|
|
20
|
-
"@graphql-mesh/transform-prune": "
|
|
21
|
-
"@graphql-mesh/transform-rename": "
|
|
22
|
-
"@graphql-mesh/transform-replace-field": "
|
|
23
|
-
"@graphql-mesh/transform-resolvers-composition": "
|
|
24
|
-
"@graphql-mesh/transform-type-merging": "
|
|
25
|
-
"@graphql-mesh/types": "
|
|
26
|
-
"graphql": "
|
|
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;
|