@adobe/aio-cli-plugin-api-mesh 2.0.0 → 2.2.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.
Files changed (32) hide show
  1. package/oclif.manifest.json +1 -1
  2. package/package.json +5 -3
  3. package/src/commands/__fixtures__/env_invalid +8 -0
  4. package/src/commands/__fixtures__/env_valid +3 -0
  5. package/src/commands/__fixtures__/files/requestParams.json +3 -0
  6. package/src/commands/__fixtures__/openapi-schema.json +4 -0
  7. package/src/commands/__fixtures__/requestParams.json +3 -0
  8. package/src/commands/__fixtures__/sample_fully_qualified_mesh.json +29 -0
  9. package/src/commands/__fixtures__/sample_invalid_mesh.txt +17 -0
  10. package/src/commands/__fixtures__/sample_mesh_files.json +23 -0
  11. package/src/commands/__fixtures__/sample_mesh_invalid_file_content.json +14 -0
  12. package/src/commands/__fixtures__/sample_mesh_invalid_file_name.json +27 -0
  13. package/src/commands/__fixtures__/sample_mesh_invalid_paths.json +23 -0
  14. package/src/commands/__fixtures__/sample_mesh_invalid_type.json +27 -0
  15. package/src/commands/__fixtures__/sample_mesh_mismatching_path.json +29 -0
  16. package/src/commands/__fixtures__/sample_mesh_outside_workspace_dir.json +23 -0
  17. package/src/commands/__fixtures__/sample_mesh_path_from_home.json +14 -0
  18. package/src/commands/__fixtures__/sample_mesh_subdirectory.json +23 -0
  19. package/src/commands/__fixtures__/sample_mesh_with_files_array.json +29 -0
  20. package/src/commands/__fixtures__/sample_mesh_with_placeholder +17 -0
  21. package/src/commands/api-mesh/__tests__/create.test.js +1334 -140
  22. package/src/commands/api-mesh/__tests__/delete.test.js +3 -3
  23. package/src/commands/api-mesh/__tests__/init.test.js +390 -0
  24. package/src/commands/api-mesh/__tests__/update.test.js +524 -109
  25. package/src/commands/api-mesh/create.js +47 -14
  26. package/src/commands/api-mesh/init.js +168 -0
  27. package/src/commands/api-mesh/update.js +49 -17
  28. package/src/helpers.js +254 -3
  29. package/src/lib/devConsole.js +1 -2
  30. package/src/templates/gitignore +1 -0
  31. package/src/templates/package.json +38 -0
  32. package/src/utils.js +337 -33
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "",
3
+ "version": "0.0.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
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"
27
+ },
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "keywords": [
32
+ "api-mesh"
33
+ ],
34
+ "license": "Apache-2.0",
35
+ "scripts": {},
36
+ "description": "API mesh starter template",
37
+ "author": "Adobe Inc."
38
+ }
package/src/utils.js CHANGED
@@ -1,40 +1,22 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const logger = require('../src/classes/logger');
4
+ const { Flags } = require('@oclif/core');
5
+ const { readFile } = require('fs/promises');
6
+ const { interpolateMesh } = require('./helpers');
7
+ const dotenv = require('dotenv');
8
+
1
9
  /**
2
- * Returns the string representation of the object's path.
3
- * If the path evaluates to false, the default string is returned.
4
- *
5
- * @param {object} obj
6
- * @param {Array<string>} path
7
- * @param {string} defaultString
8
- * @returns {string}
10
+ * @returns returns the root directory of the project
9
11
  */
10
- function objToString(obj, path = [], defaultString = '') {
11
- try {
12
- // Cache the current object
13
- let current = obj;
14
-
15
- // For each item in the path, dig into the object
16
- for (let i = 0; i < path.length; i++) {
17
- // If the item isn't found, return the default (or null)
18
- if (!current[path[i]]) return defaultString;
19
-
20
- // Otherwise, update the current value
21
- current = current[path[i]];
22
- }
23
-
24
- if (typeof current === 'string') {
25
- return current;
26
- } else if (typeof current === 'object') {
27
- return JSON.stringify(current, null, 2);
28
- } else {
29
- return defaultString;
30
- }
31
- } catch (error) {
32
- return defaultString;
12
+ function getAppRootDir() {
13
+ let currentDir = __dirname;
14
+ while (!fs.existsSync(path.join(currentDir, 'package.json'))) {
15
+ currentDir = path.join(currentDir, '..');
33
16
  }
17
+ return currentDir;
34
18
  }
35
19
 
36
- const { Flags } = require('@oclif/core');
37
-
38
20
  const ignoreCacheFlag = Flags.boolean({
39
21
  char: 'i',
40
22
  description: 'Ignore cache and force manual org -> project -> workspace selection',
@@ -53,9 +35,331 @@ const jsonFlag = Flags.boolean({
53
35
  default: false,
54
36
  });
55
37
 
38
+ const envFileFlag = Flags.string({
39
+ char: 'e',
40
+ description: 'Path to env file',
41
+ default: '.env',
42
+ });
43
+
44
+ /**
45
+ * Parse the meshConfig and get the list of (local) files to be imported
46
+ *
47
+ * @param data MeshConfig
48
+ * @param meshConfigName MeshConfig
49
+ * @returns files[] files present in meshConfig
50
+ */
51
+ function getFilesInMeshConfig(data, meshConfigName) {
52
+ //ignore if the file names start with http or https
53
+ const fileURLRegex = /^(http|s:\/\/)/;
54
+
55
+ let filesList = [];
56
+
57
+ data.meshConfig.sources.forEach(source => {
58
+ // JSONSchema handler
59
+ source.handler.JsonSchema?.operations.forEach(operation => {
60
+ if (operation.requestSchema && !fileURLRegex.test(operation.requestSchema)) {
61
+ filesList.push(operation.requestSchema);
62
+ }
63
+ if (operation.responseSchema && !fileURLRegex.test(operation.responseSchema)) {
64
+ filesList.push(operation.responseSchema);
65
+ }
66
+ if (operation.requestSample && !fileURLRegex.test(operation.requestSample)) {
67
+ filesList.push(operation.requestSample);
68
+ }
69
+ if (operation.responseSample && !fileURLRegex.test(operation.responseSample)) {
70
+ filesList.push(operation.responseSample);
71
+ }
72
+ });
73
+
74
+ // OpenAPI handler
75
+ if (source.handler.openapi && !fileURLRegex.test(source.handler.openapi.source)) {
76
+ filesList.push(source.handler.openapi.source);
77
+ }
78
+ });
79
+
80
+ // Additional Resolvers
81
+ data.meshConfig.additionalResolvers?.forEach(additionalResolver => {
82
+ if (!fileURLRegex.test(additionalResolver)) {
83
+ filesList.push(additionalResolver);
84
+ }
85
+ });
86
+
87
+ // ReplaceField Transform - source level
88
+ data.meshConfig.sources.transforms?.forEach(transform => {
89
+ transform.replaceField?.replacements.forEach(replacement => {
90
+ if (replacement.composer && !fileURLRegex.test(replacement.composer)) {
91
+ filesList.push(replacement.composer);
92
+ }
93
+ });
94
+ });
95
+
96
+ // ReplaceField Transform - mesh level
97
+ data.meshConfig.transforms?.forEach(transform => {
98
+ transform.replaceField?.replacements.forEach(replacement => {
99
+ if (replacement.composer && !fileURLRegex.test(replacement.composer)) {
100
+ filesList.push(replacement.composer);
101
+ }
102
+ });
103
+ });
104
+
105
+ try {
106
+ if (filesList.length) {
107
+ checkFilesAreUnderMeshDirectory(filesList, meshConfigName);
108
+ validateFileType(filesList);
109
+ validateFileName(filesList, data);
110
+ }
111
+ } catch (err) {
112
+ logger.error(err.message);
113
+ throw new Error(err.message);
114
+ }
115
+
116
+ return filesList;
117
+ }
118
+
119
+ /**
120
+ * Checks if files are in the same directory or subdirectories of mesh
121
+ *
122
+ * @param data MeshConfig
123
+ * @param meshConfigName MeshConfig
124
+ */
125
+ function checkFilesAreUnderMeshDirectory(filesList, meshConfigName) {
126
+ //handle files that are outside to the directory and subdirectories of meshConfig
127
+ let invalidPaths = [];
128
+ for (let i = 0; i < filesList.length; i++) {
129
+ if (
130
+ !path
131
+ .resolve(path.dirname(meshConfigName), filesList[i])
132
+ .includes(path.resolve(path.dirname(meshConfigName))) ||
133
+ filesList[i].includes('~')
134
+ ) {
135
+ invalidPaths.push(path.basename(filesList[i]));
136
+ }
137
+ }
138
+
139
+ filesOutsideRootDir(invalidPaths);
140
+ }
141
+
142
+ /**
143
+ * Error out if the files are outside the mesh directory
144
+ *
145
+ * @param invalidPaths Array
146
+ */
147
+ function filesOutsideRootDir(invalidPaths) {
148
+ if (invalidPaths.length) {
149
+ throw new Error(`File(s): ${invalidPaths.join(', ')} is outside the mesh directory.`);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Check if there are any placeholders in the input mesh file
155
+ * The below regular expressions are part of pupa string interpolation
156
+ * doubleBraceRegex = /{{(\d+|[a-z$_][\w\-$]*?(?:\.[\w\-$]*?)*?)}}/gi
157
+ * braceRegex = /{(\d+|[a-z$_][\w\-$]*?(?:\.[\w\-$]*?)*?)}/gi
158
+ * The above regex has been enhanced to include prefix '.env'
159
+ * @param {string} mesh
160
+ * @returns {boolean}
161
+ */
162
+
163
+ function checkPlaceholders(mesh) {
164
+ const doubleBraceRegex = /{{env\.(\d+|[a-z$_][\w\-$]*?(?:\.[\w\-$]*?)*?)}}/gi;
165
+ const braceRegex = /{env\.(\d+|[a-z$_][\w\-$]*?(?:\.[\w\-$]*?)*?)}/gi;
166
+ const foundDoubleBraceRegex = mesh.match(doubleBraceRegex);
167
+ const foundSingleBraceRegex = mesh.match(braceRegex);
168
+
169
+ if (
170
+ typeof foundDoubleBraceRegex === 'object' &&
171
+ foundDoubleBraceRegex === null &&
172
+ typeof foundSingleBraceRegex === 'object' &&
173
+ foundSingleBraceRegex === null
174
+ ) {
175
+ return false;
176
+ }
177
+ return true;
178
+ }
179
+
180
+ /**
181
+ * Read the file contents. If there is any error, report it to the user.
182
+ * @param {string} file
183
+ * @param {object} command
184
+ * @param {string} filetype
185
+ * @returns {string}
186
+ */
187
+ async function readFileContents(file, command, filetype) {
188
+ try {
189
+ return await readFile(file, 'utf8');
190
+ } catch (error) {
191
+ command.log(error.message);
192
+ if (filetype === 'mesh') {
193
+ command.error(
194
+ `Unable to read the mesh configuration file provided. Please check the file and try again.`,
195
+ );
196
+ }
197
+ command.error(`Unable to read the file ${file}. Please check the file and try again.`);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Check if the files are of valid types .js, .json
203
+ *
204
+ * @param filesList List of files in mesh config
205
+ */
206
+ function validateFileType(filesList) {
207
+ const filesWithInvalidTypes = [];
208
+
209
+ filesList.forEach(file => {
210
+ const extension = path.extname(file);
211
+ const isValidFileType = ['.js', '.json'].includes(extension);
212
+
213
+ if (!isValidFileType) {
214
+ filesWithInvalidTypes.push(path.basename(file));
215
+ }
216
+ });
217
+
218
+ if (filesWithInvalidTypes.length) {
219
+ throw new Error(
220
+ `Mesh files must be JavaScript or JSON. Other file types are not supported. The following file(s) are invalid: ${filesWithInvalidTypes}.`,
221
+ );
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Validate the filenames
227
+ *
228
+ * @param filesList Files in sources, tranforms or additionalResolvers in the meshConfig
229
+ * @param data MeshConfig
230
+ */
231
+ function validateFileName(filesList, data) {
232
+ const filesWithInvalidNames = [];
233
+
234
+ // Check if the file names are less than 25 characters
235
+ filesList.forEach(file => {
236
+ const fileName = path.basename(file);
237
+ if (fileName.length > 25) {
238
+ filesWithInvalidNames.push(fileName);
239
+ }
240
+ });
241
+
242
+ if (filesWithInvalidNames.length) {
243
+ throw new Error(
244
+ `Mesh file names must be less than 25 characters. The following file(s) are invalid: ${filesWithInvalidNames}.`,
245
+ );
246
+ }
247
+
248
+ // check if the the filePaths in the files array match
249
+ // the fileNames in sources, transforms or additionalResolvers
250
+
251
+ if (data.meshConfig.files) {
252
+ for (let i = 0; i < data.meshConfig.files.length; i++) {
253
+ if (filesList.indexOf(data.meshConfig.files[i].path) == -1) {
254
+ throw new Error(`Please make sure the file names are matching in meshConfig.`);
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ /**validates the environment file content
261
+ * @param {string} envContent
262
+ * @returns {object} containing the status of validation
263
+ * If validation is failed then the error property including the formatting errors is returned.
264
+ */
265
+ function validateEnvFileFormat(envContent) {
266
+ //Key should start with a underscore or an alphabet followed by underscore/alphanumeric characters
267
+ const envKeyRegex = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/;
268
+
269
+ const envValueRegex = /^(?:"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|[^'"\s])+$/;
270
+
271
+ /*
272
+ The above regex matches one or more of below :
273
+ (?:"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|[^'"\s])
274
+ which is
275
+ 1. ?:"(?:\\.|[^\\"])*" : Non capturing group starts and ends with '"'
276
+ */
277
+ const envDict = {};
278
+ const lines = envContent.split(/\r?\n/);
279
+ const errors = [];
280
+
281
+ for (let index = 0; index < lines.length; index++) {
282
+ const line = lines[index];
283
+ const trimmedLine = line.trim();
284
+ if (trimmedLine.startsWith('#') || trimmedLine === '') {
285
+ // ignore comment or empty lines
286
+ continue;
287
+ }
288
+
289
+ if (!trimmedLine.includes('=')) {
290
+ errors.push(`Invalid format << ${trimmedLine} >> on line ${index + 1}`);
291
+ } else {
292
+ const [key, value] = trimmedLine.split('=', 2);
293
+ if (!envKeyRegex.test(key) || !envValueRegex.test(value)) {
294
+ // invalid format: key or value does not match regex
295
+ errors.push(`Invalid format for key/value << ${trimmedLine} >> on line ${index + 1}`);
296
+ }
297
+ if (key in envDict) {
298
+ // duplicate key found
299
+ errors.push(`Duplicate key << ${key} >> on line ${index + 1}`);
300
+ }
301
+ envDict[key] = value;
302
+ }
303
+ }
304
+ if (errors.length) {
305
+ return {
306
+ valid: false,
307
+ error: errors.toString(),
308
+ };
309
+ }
310
+ return {
311
+ valid: true,
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Read the environment file, checks for validation status and interpolate mesh
317
+ * @param {string} inputMeshData
318
+ * @param {string} envFilePath
319
+ * @param {object} command
320
+ * @returns {string}
321
+ */
322
+ async function validateAndInterpolateMesh(inputMeshData, envFilePath, command) {
323
+ //Read the environment file
324
+ const envFileContent = await readFileContents(envFilePath, command, 'env');
325
+
326
+ //Validate the environment file
327
+ const envFileValidity = validateEnvFileFormat(envFileContent);
328
+ if (envFileValidity.valid) {
329
+ //load env file using dotenv and add 'env' as the root property in the object
330
+ const envObj = { env: dotenv.config({ path: envFilePath }).parsed };
331
+ const { interpolationStatus, missingKeys, interpolatedMeshData } = await interpolateMesh(
332
+ inputMeshData,
333
+ envObj,
334
+ );
335
+
336
+ if (interpolationStatus == 'failed') {
337
+ command.error(
338
+ 'The mesh file cannot be interpolated due to missing keys : ' + missingKeys.join(' , '),
339
+ );
340
+ }
341
+
342
+ try {
343
+ return JSON.parse(interpolatedMeshData);
344
+ } catch (err) {
345
+ command.log(err.message);
346
+ command.log(interpolatedMeshData);
347
+ command.error('Interpolated mesh is not a valid JSON. Please check the generated json file.');
348
+ }
349
+ } else {
350
+ command.error(`Issue in ${envFilePath} file - ` + envFileValidity.error);
351
+ }
352
+ }
353
+
56
354
  module.exports = {
57
- objToString,
58
355
  ignoreCacheFlag,
59
356
  autoConfirmActionFlag,
60
357
  jsonFlag,
358
+ getFilesInMeshConfig,
359
+ envFileFlag,
360
+ checkPlaceholders,
361
+ readFileContents,
362
+ validateEnvFileFormat,
363
+ validateAndInterpolateMesh,
364
+ getAppRootDir,
61
365
  };