@adobe/aio-cli-plugin-api-mesh 3.0.1 → 3.1.0-beta.2

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.
@@ -70,6 +70,18 @@ class InitCommand extends Command {
70
70
  await fs.writeFile(filePath, JSON.stringify(pkgJSON, null, 2), 'utf8', { mode: 'w' });
71
71
  }
72
72
 
73
+ async createDotNpmrcFile(templatePath, filePath) {
74
+ const dotNpmrcFile = await fs.readFile(templatePath, 'utf8');
75
+
76
+ await fs.writeFile(filePath, dotNpmrcFile, 'utf8', { mode: 'w' });
77
+ }
78
+
79
+ async createGitIgnoreFile(templatePath, filePath) {
80
+ const gitIgnoreFile = await fs.readFile(templatePath, 'utf8');
81
+
82
+ await fs.writeFile(filePath, gitIgnoreFile, 'utf8', { mode: 'w' });
83
+ }
84
+
73
85
  async run() {
74
86
  const { args, flags } = await this.parse(InitCommand);
75
87
  const gitFlagOptions = {
@@ -81,6 +93,7 @@ class InitCommand extends Command {
81
93
  let shouldCreateGit = gitFlagOptions[flags.git];
82
94
  let packageManagerChoice = flags.packageManager;
83
95
  const packageJsonTemplate = `${getAppRootDir()}/src/templates/package.json`;
96
+ const dotNpmrcPath = `${getAppRootDir()}/src/templates/npmrc`;
84
97
  const shouldCreateWorkspace = await promptConfirm(
85
98
  `Do you want to create the workspace in ${absolutePath}`,
86
99
  );
@@ -124,11 +137,10 @@ class InitCommand extends Command {
124
137
  try {
125
138
  await runCliCommand('git init', absolutePath);
126
139
 
127
- const gitIgnoreTemplate = `${getAppRootDir()}/src/templates/gitignore`;
140
+ const gitIgnoreTemplatePath = `${getAppRootDir()}/src/templates/gitignore`;
141
+ const gitIgnoreFilePath = `${absolutePath}/.gitignore`;
128
142
 
129
- await fs.writeFile(`${absolutePath}/.gitignore`, gitIgnoreTemplate, 'utf8', {
130
- mode: 'w',
131
- });
143
+ await this.createGitIgnoreFile(gitIgnoreTemplatePath, gitIgnoreFilePath);
132
144
  } catch (error) {
133
145
  this.error(error);
134
146
  }
@@ -144,6 +156,8 @@ class InitCommand extends Command {
144
156
  args.projectName,
145
157
  );
146
158
 
159
+ await this.createDotNpmrcFile(dotNpmrcPath, `${absolutePath}/.npmrc`);
160
+
147
161
  if (packageManagerChoice === 'npm') {
148
162
  try {
149
163
  await runCliCommand(`npm install`, absolutePath);
@@ -0,0 +1,156 @@
1
+ /*
2
+ Copyright 2021 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ Unless required by applicable law or agreed to in writing, software distributed under
7
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
8
+ OF ANY KIND, either express or implied. See the License for the specific language
9
+ governing permissions and limitations under the License.
10
+ */
11
+
12
+ const { Command } = require('@oclif/core');
13
+ const {
14
+ portNoFlag,
15
+ debugFlag,
16
+ envFileFlag,
17
+ autoConfirmActionFlag,
18
+ readFileContents,
19
+ validateAndInterpolateMesh,
20
+ checkPlaceholders,
21
+ getFilesInMeshConfig,
22
+ } = require('../../utils');
23
+ const meshBuilder = require('@adobe-apimesh/mesh-builder');
24
+ const fs = require('fs');
25
+ const UUID = require('../../uuid');
26
+ const path = require('path');
27
+ const { initRequestId, startGraphqlServer, importFiles } = require('../../helpers');
28
+ const logger = require('../../classes/logger');
29
+ require('dotenv').config();
30
+
31
+ const { validateMesh, buildMesh, compileMesh } = meshBuilder.default;
32
+
33
+ class RunCommand extends Command {
34
+ static summary = 'Run local development server';
35
+ static description = 'Run a local development server that builds and compiles a mesh locally';
36
+
37
+ static args = [
38
+ {
39
+ name: 'file',
40
+ description: 'Mesh File',
41
+ },
42
+ ];
43
+
44
+ static flags = {
45
+ port: portNoFlag,
46
+ debug: debugFlag,
47
+ env: envFileFlag,
48
+ autoConfirmAction: autoConfirmActionFlag,
49
+ };
50
+
51
+ static enableJsonFlag = true;
52
+
53
+ static examples = [];
54
+
55
+ async run() {
56
+ await initRequestId();
57
+
58
+ logger.info(`RequestId: ${global.requestId}`);
59
+
60
+ const { args, flags } = await this.parse(RunCommand);
61
+
62
+ if (!args.file) {
63
+ throw new Error('Missing file path. Run aio api-mesh run --help for more info.');
64
+ }
65
+
66
+ let portNo;
67
+
68
+ //To set the port number using the environment file
69
+ if (process.env.PORT !== undefined) {
70
+ if (isNaN(process.env.PORT) || !Number.isInteger(parseInt(process.env.PORT))) {
71
+ throw new Error('PORT value in the .env file is not a valid integer');
72
+ }
73
+
74
+ portNo = process.env.PORT;
75
+ }
76
+
77
+ //To set the port number as the provided value in the command
78
+ if (flags.port !== undefined) {
79
+ portNo = flags.port;
80
+ }
81
+
82
+ //To set the default port to 5000
83
+ if (!portNo) {
84
+ portNo = 5000;
85
+ }
86
+
87
+ const envFilePath = await flags.env;
88
+
89
+ try {
90
+ //Ensure that current directory includes package.json
91
+ if (fs.existsSync(path.join(process.cwd(), 'package.json'))) {
92
+ //Read the mesh input file
93
+ let inputMeshData = await readFileContents(args.file, this, 'mesh');
94
+ let data;
95
+
96
+ if (checkPlaceholders(inputMeshData)) {
97
+ this.log('The provided mesh contains placeholders. Starting mesh interpolation process.');
98
+ data = await validateAndInterpolateMesh(inputMeshData, envFilePath, this);
99
+ } else {
100
+ try {
101
+ data = JSON.parse(inputMeshData);
102
+ } catch (err) {
103
+ this.log(err.message);
104
+ throw new Error('Input mesh file is not a valid JSON. Please check the file provided.');
105
+ }
106
+ }
107
+
108
+ let filesList = [];
109
+
110
+ try {
111
+ filesList = getFilesInMeshConfig(data, args.file);
112
+ } catch (err) {
113
+ this.log(err.message);
114
+ throw new Error('Input mesh config is not valid.');
115
+ }
116
+
117
+ // if local files are present, import them in files array in meshConfig
118
+ if (filesList.length) {
119
+ try {
120
+ // minification of js will not be done for run command if debugging is enabled
121
+ data = await importFiles(
122
+ data,
123
+ filesList,
124
+ args.file,
125
+ flags.autoConfirmAction,
126
+ !flags.debug,
127
+ );
128
+ } catch (err) {
129
+ this.log(err.message);
130
+ throw new Error(
131
+ 'Unable to import the files in the mesh config. Please check the file and try again.',
132
+ );
133
+ }
134
+ }
135
+
136
+ //Generating unique mesh id
137
+ let meshId = UUID.newUuid().toString();
138
+
139
+ await validateMesh(data.meshConfig);
140
+ await buildMesh(meshId, data.meshConfig);
141
+ await compileMesh(meshId);
142
+
143
+ this.log(`Starting server on port : ${portNo}`);
144
+ await startGraphqlServer(meshId, portNo, flags.debug);
145
+ } else {
146
+ throw new Error(
147
+ '`aio api-mesh run` cannot be executed because there is no package.json file in the current directory. Use `aio api-mesh init` to set up a package.',
148
+ );
149
+ }
150
+ } catch (error) {
151
+ this.error(error.message);
152
+ }
153
+ }
154
+ }
155
+
156
+ module.exports = RunCommand;
package/src/helpers.js CHANGED
@@ -522,7 +522,13 @@ async function promptInput(message) {
522
522
  * @param meshConfigName MeshConfigName
523
523
  * @param autoConfirmActionFlag The user won't be prompted any questions, if this flag is set
524
524
  */
525
- async function importFiles(data, filesListArray, meshConfigName, autoConfirmActionFlag) {
525
+ async function importFiles(
526
+ data,
527
+ filesListArray,
528
+ meshConfigName,
529
+ autoConfirmActionFlag,
530
+ shouldMinifyJS = true,
531
+ ) {
526
532
  //if autoConfirmActionFlag is passed in the command, it should override by default
527
533
  let shouldOverride = true;
528
534
  let filesNotFound = [];
@@ -558,7 +564,7 @@ async function importFiles(data, filesListArray, meshConfigName, autoConfirmActi
558
564
  } else {
559
565
  //if file does not exist in files array, but exists in filesystem, we append
560
566
  if (fs.existsSync(path.resolve(path.dirname(meshConfigName), file))) {
561
- resultData = updateFilesArray(resultData, file, meshConfigName, -1);
567
+ resultData = updateFilesArray(resultData, file, meshConfigName, -1, shouldMinifyJS);
562
568
  } else {
563
569
  filesNotFound.push(file);
564
570
  }
@@ -588,6 +594,7 @@ async function importFiles(data, filesListArray, meshConfigName, autoConfirmActi
588
594
  overrideArr[i].fileName,
589
595
  meshConfigName,
590
596
  overrideArr[i].index,
597
+ shouldMinifyJS,
591
598
  );
592
599
  }
593
600
  }
@@ -673,7 +680,7 @@ function runCliCommand(command, workingDirectory = '.') {
673
680
  * @param meshConfigName MeshConfig name
674
681
  * @param index Append operation if index is -1, else override, it is the index where the override takes place
675
682
  */
676
- function updateFilesArray(data, file, meshConfigName, index) {
683
+ function updateFilesArray(data, file, meshConfigName, index, shouldMinifyJS = true) {
677
684
  try {
678
685
  let readFileData = fs.readFileSync(
679
686
  path.resolve(path.dirname(meshConfigName), file),
@@ -695,8 +702,17 @@ function updateFilesArray(data, file, meshConfigName, index) {
695
702
  throw new Error(`Invalid JSON content in ${path.basename(file)}`);
696
703
  }
697
704
 
698
- //data to be overridden or appended
699
- const dataInFilesArray = jsmin(readFileData);
705
+ // shouldMinifyJS would be always true for create and update commands
706
+ // if run command run on debug mode it will be false and js files wont be minified
707
+ if (shouldMinifyJS) {
708
+ readFileData = jsmin(readFileData);
709
+ } else {
710
+ if (path.extname(file) !== '.js') {
711
+ readFileData = jsmin(readFileData);
712
+ }
713
+ }
714
+
715
+ const dataInFilesArray = readFileData;
700
716
 
701
717
  if (index >= 0) {
702
718
  data.meshConfig.files[index] = {
@@ -723,6 +739,42 @@ function updateFilesArray(data, file, meshConfigName, index) {
723
739
  }
724
740
  }
725
741
 
742
+ /**
743
+ * Start GraphQL server for a particular mesh on a particular port
744
+ *
745
+ * @param meshId MeshId of the mesh
746
+ * @param port Port number at which the server is to be started
747
+ * @param debug Boolean flag to set the debug mode
748
+ */
749
+ function startGraphqlServer(meshId, port, debug) {
750
+ const serverPath = `${__dirname}/server.js ${meshId} ${port}`;
751
+ const command = debug
752
+ ? `node --inspect-brk --trace-warnings ${serverPath}`
753
+ : `node ${serverPath}`;
754
+
755
+ const server = exec(command);
756
+
757
+ server.stdout.on('data', data => {
758
+ console.log(data);
759
+ });
760
+
761
+ server.stderr.on('data', data => {
762
+ console.error(data);
763
+ });
764
+
765
+ server.on('close', code => {
766
+ console.log(`Server closed with code ${code}`);
767
+ });
768
+
769
+ server.on('exit', code => {
770
+ console.log(`Server exited with code ${code}`);
771
+ });
772
+
773
+ server.on('error', err => {
774
+ console.error(`Server exited with error ${err}`);
775
+ });
776
+ }
777
+
726
778
  module.exports = {
727
779
  objToString,
728
780
  promptInput,
@@ -737,4 +789,5 @@ module.exports = {
737
789
  interpolateMesh,
738
790
  runCliCommand,
739
791
  updateFilesArray,
792
+ startGraphqlServer,
740
793
  };
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
+ );