@adobe/aio-cli-plugin-api-mesh 4.0.1-beta.1 → 4.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 +8 -4
- package/src/commands/api-mesh/__tests__/run.test.js +22 -19
- package/src/commands/api-mesh/run.js +32 -7
- package/src/cors.js +28 -0
- package/src/fixPlugins.js +28 -0
- package/src/index.js +44 -0
- package/src/plugins/complianceHeaders/complianceHeaders.js +55 -0
- package/src/plugins/complianceHeaders/index.js +2 -0
- package/src/plugins/httpDetailsExtensions/LICENSE +21 -0
- package/src/plugins/httpDetailsExtensions/index.js +81 -0
- package/src/secrets.js +34 -0
- package/src/served.js +22 -0
- package/src/server.js +30 -198
- package/src/serverUtils.js +3 -3
- package/src/wranglerServer.js +75 -0
package/oclif.manifest.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"4.0
|
|
1
|
+
{"version":"4.1.0-beta.1","commands":{"PLUGINNAME":{"id":"PLUGINNAME","description":"Your description here","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio PLUGINNAME:some_command"],"flags":{"someflag":{"name":"someflag","type":"option","char":"f","description":"this is some flag"}},"args":[]},"api-mesh:create":{"id":"api-mesh:create","description":"Create a mesh with the given config.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"json":{"name":"json","type":"boolean","description":"Output JSON","allowNo":false},"env":{"name":"env","type":"option","char":"e","description":"Path to env file","default":".env"},"secrets":{"name":"secrets","type":"option","char":"s","description":"Path to secrets file","default":false}},"args":[{"name":"file"}]},"api-mesh:delete":{"id":"api-mesh:delete","description":"Delete the config of a given mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false}},"args":[]},"api-mesh:describe":{"id":"api-mesh:describe","description":"Get details of a mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[]},"api-mesh:get":{"id":"api-mesh:get","description":"Get the config of a given mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"json":{"name":"json","type":"boolean","description":"Output JSON","allowNo":false}},"args":[{"name":"file"}]},"api-mesh:init":{"id":"api-mesh:init","description":"This command will create a workspace where you can organise your API mesh configuration and other files","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":[{"description":"API mesh workspace init","command":"aio api-mesh init commerce-mesh"},{"description":"API mesh workspace init with flags","command":"aio api-mesh init commerce-mesh --path ./mesh_projects/test_mesh --git y --packageManager yarn"}],"flags":{"path":{"name":"path","type":"option","char":"p","default":"."},"packageManager":{"name":"packageManager","type":"option","char":"m","options":["npm","yarn"]},"git":{"name":"git","type":"option","char":"g","options":["y","n"]}},"args":[{"name":"projectName","description":"Project name","required":true}]},"api-mesh:log-get-bulk":{"id":"api-mesh:log-get-bulk","description":"Download all mesh logs for a selected time period.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"startTime":{"name":"startTime","type":"option","description":"Start time for the logs in UTC","required":true},"endTime":{"name":"endTime","type":"option","description":"End time for the logs in UTC","required":true},"filename":{"name":"filename","type":"option","description":"Path to the output file for logs","required":true}},"args":[]},"api-mesh:log-get":{"id":"api-mesh:log-get","description":"Get the Log of a given mesh by RayId","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[{"name":"rayId","description":"Fetch a single log by rayID","required":true}]},"api-mesh:log-list":{"id":"api-mesh:log-list","description":"Get recent logs of requests made to the API Mesh.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"filename":{"name":"filename","type":"option","description":"Name of CSV file to export the recent logs to"}},"args":[]},"api-mesh:run":{"id":"api-mesh:run","description":"Run a local development server that builds and compiles a mesh locally","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":[],"flags":{"port":{"name":"port","type":"option","char":"p","description":"Port number for the local dev server"},"debug":{"name":"debug","type":"boolean","description":"Enable debugging mode","allowNo":false},"env":{"name":"env","type":"option","char":"e","description":"Path to env file","default":".env"},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"select":{"name":"select","type":"boolean","description":"Retrieve existing artifacts from the mesh","allowNo":false},"secrets":{"name":"secrets","type":"option","char":"s","description":"Path to secrets file","default":false}},"args":[{"name":"file","description":"Mesh File"}]},"api-mesh:status":{"id":"api-mesh:status","description":"Get a mesh status with a given meshid.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[]},"api-mesh:update":{"id":"api-mesh:update","description":"Update a mesh with the given config.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"env":{"name":"env","type":"option","char":"e","description":"Path to env file","default":".env"},"secrets":{"name":"secrets","type":"option","char":"s","description":"Path to secrets file","default":false}},"args":[{"name":"file"}]},"api-mesh:source:discover":{"id":"api-mesh:source:discover","description":"Return the list of avaliable sources","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm install action prompt. CLI will not check ask user to install source.","allowNo":false}},"args":[]},"api-mesh:source:get":{"id":"api-mesh:source:get","description":"Command returns the content of a specific source.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio api-mesh:source:get -s=<version>@<source_name>","$ aio api-mesh:source:get -s<source_name>","$ aio api-mesh:source:get -m"],"flags":{"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm print action prompt. CLI will not check ask user to print source.","allowNo":false},"source":{"name":"source","type":"option","char":"s","description":"Source name"},"multiple":{"name":"multiple","type":"boolean","char":"m","description":"Select multiple sources","allowNo":false}},"args":[]},"api-mesh:source:install":{"id":"api-mesh:source:install","description":"Command to install the source to your API mesh.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio api-mesh:source:install <version>@<source_name>","$ aio api-mesh:source:install <source_name> -v <variable_name>=<variable_value>","$ aio api-mesh:source:install <source_name> -f <path_to_variables_file>"],"flags":{"source":{"name":"source","type":"option","char":"s","description":"Source name"},"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm override action prompt. CLI will not check ask user to override source.","allowNo":false},"variable":{"name":"variable","type":"option","char":"v","description":"Variables required for the source"},"variable-file":{"name":"variable-file","type":"option","char":"f","description":"Variables file path"},"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[{"name":"source"}]}}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/aio-cli-plugin-api-mesh",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0-beta.1",
|
|
4
4
|
"description": "Adobe I/O CLI plugin to develop and manage API mesh sources",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"oclif-plugin"
|
|
@@ -45,11 +45,13 @@
|
|
|
45
45
|
"@adobe/aio-lib-ims": "^7.0.1",
|
|
46
46
|
"@adobe/plugin-hooks": "0.3.2",
|
|
47
47
|
"@adobe/plugin-on-fetch": "0.1.1",
|
|
48
|
+
"@adobe/plugin-source-headers": "^0.0.1",
|
|
48
49
|
"@graphql-mesh/cli": "0.82.30",
|
|
49
50
|
"@graphql-mesh/graphql": "0.34.13",
|
|
51
|
+
"@graphql-mesh/http": "^0.96.9",
|
|
50
52
|
"@graphql-mesh/json-schema": "0.35.38",
|
|
51
53
|
"@graphql-mesh/openapi": "0.33.39",
|
|
52
|
-
"@graphql-mesh/plugin-http-details-extensions": "0.
|
|
54
|
+
"@graphql-mesh/plugin-http-details-extensions": "^0.103.4",
|
|
53
55
|
"@graphql-mesh/runtime": "0.46.21",
|
|
54
56
|
"@graphql-mesh/soap": "0.14.25",
|
|
55
57
|
"@graphql-mesh/store": "0.9.20",
|
|
@@ -88,7 +90,7 @@
|
|
|
88
90
|
"lru-cache": "^7.14.1",
|
|
89
91
|
"node-clipboardy": "^1.0.3",
|
|
90
92
|
"node-fetch": "2.6.1",
|
|
91
|
-
"pino": "^
|
|
93
|
+
"pino": "^9.5.0",
|
|
92
94
|
"pino-pretty": "^7.6.0",
|
|
93
95
|
"pupa": "^3.1.0",
|
|
94
96
|
"source-registry-storage-adapter": "github:devx-services/source-registry-storage-adapter#main",
|
|
@@ -98,6 +100,7 @@
|
|
|
98
100
|
},
|
|
99
101
|
"devDependencies": {
|
|
100
102
|
"@babel/eslint-parser": "^7.15.8",
|
|
103
|
+
"@graphql-mesh/plugin-http-details-extensions": "0.1.21",
|
|
101
104
|
"@oclif/dev-cli": "1.26.10",
|
|
102
105
|
"@oclif/plugin-help": "2.2.3",
|
|
103
106
|
"@trivago/prettier-plugin-sort-imports": "^3.1.1",
|
|
@@ -117,7 +120,8 @@
|
|
|
117
120
|
"jest": "^29.2.2",
|
|
118
121
|
"jest-junit": "^6.0.0",
|
|
119
122
|
"prettier": "2.2.1",
|
|
120
|
-
"stdout-stderr": "^0.1.9"
|
|
123
|
+
"stdout-stderr": "^0.1.9",
|
|
124
|
+
"wrangler": "^3.80.4"
|
|
121
125
|
},
|
|
122
126
|
"engines": {
|
|
123
127
|
"node": "^16.13 || >=18.0.0",
|
|
@@ -12,7 +12,6 @@ governing permissions and limitations under the License.
|
|
|
12
12
|
|
|
13
13
|
const RunCommand = require('../run');
|
|
14
14
|
const {
|
|
15
|
-
startGraphqlServer,
|
|
16
15
|
interpolateMesh,
|
|
17
16
|
importFiles,
|
|
18
17
|
promptConfirm,
|
|
@@ -20,6 +19,7 @@ const {
|
|
|
20
19
|
initSdk,
|
|
21
20
|
writeSecretsFile,
|
|
22
21
|
} = require('../../../helpers');
|
|
22
|
+
const { runServer } = require('../../../server');
|
|
23
23
|
const { getMeshId, getMeshArtifact } = require('../../../lib/devConsole');
|
|
24
24
|
require('@adobe-apimesh/mesh-builder');
|
|
25
25
|
|
|
@@ -31,7 +31,6 @@ jest.mock('../../../helpers', () => ({
|
|
|
31
31
|
workspaceName: 'mockWorkspaceTitle',
|
|
32
32
|
}),
|
|
33
33
|
initRequestId: jest.fn().mockResolvedValue({}),
|
|
34
|
-
startGraphqlServer: jest.fn().mockResolvedValue({}),
|
|
35
34
|
interpolateMesh: jest.fn().mockResolvedValue({}),
|
|
36
35
|
importFiles: jest.fn().mockResolvedValue(),
|
|
37
36
|
promptConfirm: jest.fn().mockResolvedValue(true),
|
|
@@ -39,6 +38,10 @@ jest.mock('../../../helpers', () => ({
|
|
|
39
38
|
writeSecretsFile: jest.fn().mockResolvedValue(),
|
|
40
39
|
}));
|
|
41
40
|
|
|
41
|
+
jest.mock('../../../server', () => ({
|
|
42
|
+
runServer: jest.fn().mockResolvedValue(),
|
|
43
|
+
}));
|
|
44
|
+
|
|
42
45
|
jest.mock('../../../lib/devConsole', () => ({
|
|
43
46
|
getMeshId: jest.fn().mockResolvedValue('mockMeshId'),
|
|
44
47
|
getMeshArtifact: jest.fn().mockResolvedValue(),
|
|
@@ -91,6 +94,10 @@ describe('run command tests', () => {
|
|
|
91
94
|
platformSpy.mockRestore();
|
|
92
95
|
});
|
|
93
96
|
|
|
97
|
+
beforeAll(() => {
|
|
98
|
+
jest.spyOn(RunCommand.prototype, 'copyMeshContent').mockImplementation(() => {});
|
|
99
|
+
});
|
|
100
|
+
|
|
94
101
|
test('snapshot run command description', () => {
|
|
95
102
|
expect(RunCommand.description).toMatchInlineSnapshot(
|
|
96
103
|
`"Run a local development server that builds and compiles a mesh locally"`,
|
|
@@ -189,11 +196,7 @@ describe('run command tests', () => {
|
|
|
189
196
|
parseSpy.mockResolvedValue(parseOutput);
|
|
190
197
|
|
|
191
198
|
await RunCommand.run();
|
|
192
|
-
expect(
|
|
193
|
-
expect.anything(),
|
|
194
|
-
parseOutput.flags.port,
|
|
195
|
-
false,
|
|
196
|
-
);
|
|
199
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), parseOutput.flags.port);
|
|
197
200
|
});
|
|
198
201
|
|
|
199
202
|
test('should use the port number provided in the .env file if there is no port', async () => {
|
|
@@ -210,7 +213,7 @@ describe('run command tests', () => {
|
|
|
210
213
|
parseSpy.mockResolvedValue(parseOutput);
|
|
211
214
|
|
|
212
215
|
await RunCommand.run();
|
|
213
|
-
expect(
|
|
216
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), process.env.PORT);
|
|
214
217
|
});
|
|
215
218
|
|
|
216
219
|
test('should use the default port if port number is not provided explicitly', async () => {
|
|
@@ -226,7 +229,7 @@ describe('run command tests', () => {
|
|
|
226
229
|
parseSpy.mockResolvedValue(parseOutput);
|
|
227
230
|
|
|
228
231
|
await RunCommand.run();
|
|
229
|
-
expect(
|
|
232
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), defaultPort);
|
|
230
233
|
});
|
|
231
234
|
|
|
232
235
|
test('should return error for run command if the mesh has placeholders and env file provided using --env flag is not found', async () => {
|
|
@@ -393,7 +396,7 @@ describe('run command tests', () => {
|
|
|
393
396
|
});
|
|
394
397
|
|
|
395
398
|
await RunCommand.run();
|
|
396
|
-
expect(
|
|
399
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), defaultPort);
|
|
397
400
|
});
|
|
398
401
|
|
|
399
402
|
// file import tests
|
|
@@ -439,7 +442,7 @@ describe('run command tests', () => {
|
|
|
439
442
|
});
|
|
440
443
|
|
|
441
444
|
await RunCommand.run();
|
|
442
|
-
expect(
|
|
445
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), defaultPort);
|
|
443
446
|
});
|
|
444
447
|
|
|
445
448
|
test('should fail if the file name is more than 25 characters', async () => {
|
|
@@ -615,7 +618,7 @@ describe('run command tests', () => {
|
|
|
615
618
|
});
|
|
616
619
|
|
|
617
620
|
await RunCommand.run();
|
|
618
|
-
expect(
|
|
621
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), defaultPort);
|
|
619
622
|
});
|
|
620
623
|
|
|
621
624
|
test('should override if prompt returns Yes, if there is files array', async () => {
|
|
@@ -662,7 +665,7 @@ describe('run command tests', () => {
|
|
|
662
665
|
|
|
663
666
|
await RunCommand.run();
|
|
664
667
|
|
|
665
|
-
expect(
|
|
668
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), defaultPort);
|
|
666
669
|
});
|
|
667
670
|
|
|
668
671
|
test('should pass for a fully-qualified meshConfig even if the file does not exist in fileSystem', async () => {
|
|
@@ -709,7 +712,7 @@ describe('run command tests', () => {
|
|
|
709
712
|
});
|
|
710
713
|
|
|
711
714
|
await RunCommand.run();
|
|
712
|
-
expect(
|
|
715
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), defaultPort);
|
|
713
716
|
});
|
|
714
717
|
|
|
715
718
|
test('should pass if the file is located in subdirectory of mesh directory', async () => {
|
|
@@ -755,7 +758,7 @@ describe('run command tests', () => {
|
|
|
755
758
|
|
|
756
759
|
await RunCommand.run();
|
|
757
760
|
|
|
758
|
-
expect(
|
|
761
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), defaultPort);
|
|
759
762
|
});
|
|
760
763
|
|
|
761
764
|
test('should fail if the file is outside the workspace directory', async () => {
|
|
@@ -926,7 +929,7 @@ describe('run command tests', () => {
|
|
|
926
929
|
|
|
927
930
|
await RunCommand.run();
|
|
928
931
|
expect(writeSecretsFile).toHaveBeenCalled();
|
|
929
|
-
expect(
|
|
932
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), defaultPort);
|
|
930
933
|
});
|
|
931
934
|
|
|
932
935
|
test('should return error if ran with secrets against windows platform with batch variables', async () => {
|
|
@@ -964,7 +967,7 @@ describe('run command tests', () => {
|
|
|
964
967
|
|
|
965
968
|
await RunCommand.run();
|
|
966
969
|
expect(writeSecretsFile).toHaveBeenCalled();
|
|
967
|
-
expect(
|
|
970
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), defaultPort);
|
|
968
971
|
});
|
|
969
972
|
|
|
970
973
|
test('should pass if ran with secrets against darwin(macOS) platform with batch variables', async () => {
|
|
@@ -979,7 +982,7 @@ describe('run command tests', () => {
|
|
|
979
982
|
|
|
980
983
|
await RunCommand.run();
|
|
981
984
|
expect(writeSecretsFile).toHaveBeenCalled();
|
|
982
|
-
expect(
|
|
985
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), defaultPort);
|
|
983
986
|
});
|
|
984
987
|
|
|
985
988
|
test('should escape variables that are preceded by backslash symbol', async () => {
|
|
@@ -996,6 +999,6 @@ describe('run command tests', () => {
|
|
|
996
999
|
'Home: rootPath\nHomeString: $HOME\nHomeWithSlash: \\rootPath\nHomeStringWithSlash: \\$HOME\n',
|
|
997
1000
|
expect.anything(),
|
|
998
1001
|
);
|
|
999
|
-
expect(
|
|
1002
|
+
expect(runServer).toHaveBeenCalledWith(expect.anything(), defaultPort);
|
|
1000
1003
|
});
|
|
1001
1004
|
});
|
|
@@ -26,12 +26,10 @@ const {
|
|
|
26
26
|
} = require('../../utils');
|
|
27
27
|
const meshBuilder = require('@adobe-apimesh/mesh-builder');
|
|
28
28
|
const fs = require('fs');
|
|
29
|
-
const UUID = require('../../uuid');
|
|
30
29
|
const path = require('path');
|
|
31
30
|
const {
|
|
32
31
|
initSdk,
|
|
33
32
|
initRequestId,
|
|
34
|
-
startGraphqlServer,
|
|
35
33
|
importFiles,
|
|
36
34
|
setUpTenantFiles,
|
|
37
35
|
writeSecretsFile,
|
|
@@ -39,6 +37,8 @@ const {
|
|
|
39
37
|
const logger = require('../../classes/logger');
|
|
40
38
|
const { getMeshId, getMeshArtifact } = require('../../lib/devConsole');
|
|
41
39
|
require('dotenv').config();
|
|
40
|
+
const { runServer } = require('../../server');
|
|
41
|
+
const { fixPlugins } = require('../../fixPlugins');
|
|
42
42
|
|
|
43
43
|
const { validateMesh, buildMesh, compileMesh } = meshBuilder.default;
|
|
44
44
|
|
|
@@ -163,13 +163,12 @@ class RunCommand extends Command {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
//Generating unique mesh id
|
|
166
|
-
meshId =
|
|
166
|
+
meshId = 'testMesh';
|
|
167
167
|
|
|
168
168
|
await validateMesh(data.meshConfig);
|
|
169
169
|
await buildMesh(meshId, data.meshConfig);
|
|
170
170
|
await compileMesh(meshId);
|
|
171
171
|
}
|
|
172
|
-
|
|
173
172
|
let portNo;
|
|
174
173
|
//secrets management
|
|
175
174
|
if (secretsFilePath) {
|
|
@@ -183,6 +182,8 @@ class RunCommand extends Command {
|
|
|
183
182
|
}
|
|
184
183
|
}
|
|
185
184
|
|
|
185
|
+
await this.copyMeshContent(meshId);
|
|
186
|
+
|
|
186
187
|
//To set the port number using the environment file
|
|
187
188
|
if (process.env.PORT !== undefined) {
|
|
188
189
|
if (isNaN(process.env.PORT) || !Number.isInteger(parseInt(process.env.PORT))) {
|
|
@@ -201,9 +202,8 @@ class RunCommand extends Command {
|
|
|
201
202
|
if (!portNo) {
|
|
202
203
|
portNo = 5000;
|
|
203
204
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
await startGraphqlServer(meshId, portNo, flags.debug);
|
|
205
|
+
meshId = '000000000000-0000-0000-0000-000000000000';
|
|
206
|
+
runServer(meshId, portNo);
|
|
207
207
|
} else {
|
|
208
208
|
throw new Error(
|
|
209
209
|
'`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.',
|
|
@@ -213,6 +213,31 @@ class RunCommand extends Command {
|
|
|
213
213
|
this.error(error.message);
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
|
+
|
|
217
|
+
async copyMeshContent(meshId) {
|
|
218
|
+
// Remove mesh artifact directory if exists
|
|
219
|
+
if (fs.existsSync('.mesh')) {
|
|
220
|
+
fs.rmdirSync('.mesh', { recursive: true });
|
|
221
|
+
}
|
|
222
|
+
// Move built mesh artifact to expect directory
|
|
223
|
+
fs.renameSync(`mesh-artifact/${meshId}`, '.mesh');
|
|
224
|
+
// Remove tenant files directory if exists
|
|
225
|
+
if (fs.existsSync('tenantFiles')) {
|
|
226
|
+
fs.rmdirSync('tenantFiles', { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
// Move built tenant files if exists
|
|
229
|
+
if (fs.existsSync('mesh-artifact/tenantFiles')) {
|
|
230
|
+
fs.cpSync('mesh-artifact/tenantFiles', '.mesh/tenantFiles', { recursive: true });
|
|
231
|
+
fs.renameSync('mesh-artifact/tenantFiles', 'tenantFiles');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await fixPlugins('.mesh/index.js');
|
|
235
|
+
|
|
236
|
+
if (fs.existsSync(`${__dirname}/../../../.mesh`)) {
|
|
237
|
+
fs.rmdirSync(`${__dirname}/../../../.mesh`, { recursive: true });
|
|
238
|
+
}
|
|
239
|
+
fs.cpSync('.mesh', `${__dirname}/../../../.mesh`, { recursive: true });
|
|
240
|
+
}
|
|
216
241
|
}
|
|
217
242
|
|
|
218
243
|
module.exports = RunCommand;
|
package/src/cors.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get default CORS options.
|
|
3
|
+
* @param env Environment.
|
|
4
|
+
*/
|
|
5
|
+
const getDefaultCorsOptions = env => {
|
|
6
|
+
return env.CORS_DEFAULT_URL
|
|
7
|
+
? {
|
|
8
|
+
origin: env.CORS_DEFAULT_URL,
|
|
9
|
+
}
|
|
10
|
+
: {};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get CORS options. Merges default CORS options with custom specific options if present in the mesh configuration.
|
|
15
|
+
* Custom CORS options overwrite the default options to ensure that specific requirements can be met without altering
|
|
16
|
+
* the default configuration.
|
|
17
|
+
* @param env Environment.
|
|
18
|
+
* @param meshConfig Mesh configuration.
|
|
19
|
+
*/
|
|
20
|
+
const getCorsOptions = (env, meshConfig) => {
|
|
21
|
+
const defaultCorsOptions = getDefaultCorsOptions(env);
|
|
22
|
+
return {
|
|
23
|
+
...defaultCorsOptions,
|
|
24
|
+
...meshConfig.responseConfig?.CORS,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
module.exports = { getCorsOptions };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
/**
|
|
4
|
+
* Modifies `index.js` of a mesh artifact to mutate plugin references for edge compatibility. Current forces
|
|
5
|
+
* `@graphql-mesh/plugin-http-details-extensions` to resolve to local fork instead.
|
|
6
|
+
*/
|
|
7
|
+
async function fixPlugins(meshArtifactPath) {
|
|
8
|
+
try {
|
|
9
|
+
console.log('Modifying mesh artifact to fix plugins for edge compatibility.');
|
|
10
|
+
const data = fs.readFileSync(meshArtifactPath, 'utf8');
|
|
11
|
+
const updatedData = data.replace(
|
|
12
|
+
/@graphql-mesh\/plugin-http-details-extensions/g,
|
|
13
|
+
'../src/plugins/httpDetailsExtensions',
|
|
14
|
+
);
|
|
15
|
+
fs.writeFileSync(meshArtifactPath, updatedData, 'utf8');
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error(err);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Execute fixPlugins if run directly from CLI
|
|
22
|
+
if (require.main === module) {
|
|
23
|
+
fixPlugins();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
fixPlugins,
|
|
28
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { bindedlogger as logger } from '../utils/logger';
|
|
2
|
+
import { getRequestId } from '../utils/requestId';
|
|
3
|
+
import { ServedTier, addServedHeader } from './served';
|
|
4
|
+
import { buildServer } from './wranglerServer';
|
|
5
|
+
|
|
6
|
+
let server;
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
setServer(newServer) {
|
|
10
|
+
server = newServer;
|
|
11
|
+
},
|
|
12
|
+
/**
|
|
13
|
+
* Fetch.
|
|
14
|
+
* @param request Request.
|
|
15
|
+
* @param env Environment.
|
|
16
|
+
* @param ctx Event context.
|
|
17
|
+
*/
|
|
18
|
+
async fetch(request, env, ctx) {
|
|
19
|
+
const requestId = getRequestId(request);
|
|
20
|
+
// Retrieve environment variables
|
|
21
|
+
const { MESH_ID: meshId, LOG_LEVEL: logLevel } = env;
|
|
22
|
+
const loggerInstance = logger({ logLevel, meshId, requestId });
|
|
23
|
+
const meshArtifacts = await import('../.mesh');
|
|
24
|
+
const rawMesh = await import('../.mesh/.meshrc.json');
|
|
25
|
+
|
|
26
|
+
if (!server) {
|
|
27
|
+
server = await this.buildAndCacheServer(env, loggerInstance, meshArtifacts, rawMesh);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
loggerInstance.debug('WORKER HOT: Fetching via worker');
|
|
31
|
+
const response = await server.fetch(request, ctx);
|
|
32
|
+
addServedHeader(response, ServedTier.WORKER_HOT);
|
|
33
|
+
return response;
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Build and cache mesh instance/server in global variable.
|
|
37
|
+
* @param env
|
|
38
|
+
* @param loggerInstance
|
|
39
|
+
*/
|
|
40
|
+
async buildAndCacheServer(env, loggerInstance, meshArtifacts, meshConfig) {
|
|
41
|
+
server = await buildServer(loggerInstance, env, meshArtifacts, meshConfig);
|
|
42
|
+
return server;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CF-Connecting-IP provides the client IP address connecting to Cloudflare to the origin web server. This header will only be sent on the
|
|
3
|
+
* traffic from Cloudflare’s edge to your origin web server. Upstream requests will the Cloudflare Worker client IP address of
|
|
4
|
+
* `2a06:98c0:3600::103`.
|
|
5
|
+
* @see https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip
|
|
6
|
+
*/
|
|
7
|
+
const CF_CONNECTING_IP_HEADER = 'cf-connecting-ip';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The X-Forwarded-For (XFF) request header is a de-facto standard header for identifying the originating IP address of a client connecting
|
|
11
|
+
* to a web server through a proxy server.
|
|
12
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
|
|
13
|
+
*/
|
|
14
|
+
const X_FORWARDED_FOR_HEADER = 'x-forwarded-for';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add `x-forwarded-for` header from request context to each fetch.
|
|
18
|
+
* @param context Request context.
|
|
19
|
+
* @param headers Fetch headers.
|
|
20
|
+
*/
|
|
21
|
+
const addXForwardedForHeader = (context, headers) => {
|
|
22
|
+
// `cf-connecting-ip` header contains the original visitor's IP address
|
|
23
|
+
const connectingIp = context?.request.headers.get(CF_CONNECTING_IP_HEADER);
|
|
24
|
+
const xForwardedFor = context?.request.headers.get(X_FORWARDED_FOR_HEADER);
|
|
25
|
+
if (connectingIp) {
|
|
26
|
+
if (!xForwardedFor) {
|
|
27
|
+
// Construct new `x-forwarded-for` header if not present in original request
|
|
28
|
+
headers.set(X_FORWARDED_FOR_HEADER, connectingIp);
|
|
29
|
+
} else {
|
|
30
|
+
// Construct `x-forwarded-for` header using original header
|
|
31
|
+
headers.set(X_FORWARDED_FOR_HEADER, `${xForwardedFor}, ${connectingIp}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Adds compliance headers to source fetch requests.
|
|
38
|
+
*/
|
|
39
|
+
function useComplianceHeaders() {
|
|
40
|
+
return {
|
|
41
|
+
onFetch({ context, options }) {
|
|
42
|
+
// Construct mutable headers from options passed to each fetch
|
|
43
|
+
const headers = new Headers(options.headers);
|
|
44
|
+
addXForwardedForHeader(context, headers);
|
|
45
|
+
options.headers = headers;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
useComplianceHeaders,
|
|
52
|
+
CF_CONNECTING_IP_HEADER,
|
|
53
|
+
X_FORWARDED_FOR_HEADER,
|
|
54
|
+
addXForwardedForHeader,
|
|
55
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Uri Goldshtein
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fork of https://github.com/ardatan/graphql-mesh/blob/%40graphql-mesh/plugin-http-details-extensions%400.1.21/packages/plugins/http-details-extensions/src/index.ts
|
|
3
|
+
* Version: 0.1.21
|
|
4
|
+
* TODO: Extract to a separate repository/artifact after approach has been validated.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* eslint-disable */
|
|
8
|
+
|
|
9
|
+
const { isAsyncIterable } = require('@envelop/core');
|
|
10
|
+
const { getHeadersObj } = require('@graphql-mesh/utils');
|
|
11
|
+
|
|
12
|
+
function useIncludeHttpDetailsInExtensions(opts) {
|
|
13
|
+
if (!opts.if) {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const httpDetailsByContext = new WeakMap();
|
|
18
|
+
|
|
19
|
+
function getHttpDetailsByContext(context) {
|
|
20
|
+
let httpDetails = httpDetailsByContext.get(context);
|
|
21
|
+
if (!httpDetails) {
|
|
22
|
+
httpDetails = [];
|
|
23
|
+
httpDetailsByContext.set(context, httpDetails);
|
|
24
|
+
}
|
|
25
|
+
return httpDetails;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
onFetch({ url, context, info, options }) {
|
|
30
|
+
if (context != null) {
|
|
31
|
+
const requestTimestamp = Date.now();
|
|
32
|
+
return ({ response }) => {
|
|
33
|
+
const responseTimestamp = Date.now();
|
|
34
|
+
const responseTime = responseTimestamp - requestTimestamp;
|
|
35
|
+
const httpDetailsList = getHttpDetailsByContext(context);
|
|
36
|
+
const httpDetails = {
|
|
37
|
+
sourceName: (info)?.sourceName,
|
|
38
|
+
path: info?.path,
|
|
39
|
+
request: {
|
|
40
|
+
timestamp: requestTimestamp,
|
|
41
|
+
url,
|
|
42
|
+
method: options.method || 'GET',
|
|
43
|
+
headers: getHeadersObj(options.headers),
|
|
44
|
+
},
|
|
45
|
+
response: {
|
|
46
|
+
timestamp: responseTimestamp,
|
|
47
|
+
status: response.status,
|
|
48
|
+
statusText: response.statusText,
|
|
49
|
+
headers: getHeadersObj(response.headers),
|
|
50
|
+
// Added to interface to account for edge fetch implementation/behavior
|
|
51
|
+
cookies: response.headers.getSetCookie(),
|
|
52
|
+
},
|
|
53
|
+
responseTime,
|
|
54
|
+
};
|
|
55
|
+
httpDetailsList.push(httpDetails);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
},
|
|
60
|
+
onExecute({ args: { contextValue } }) {
|
|
61
|
+
return {
|
|
62
|
+
onExecuteDone({ result, setResult }) {
|
|
63
|
+
if (!isAsyncIterable(result)) {
|
|
64
|
+
const httpDetailsList = httpDetailsByContext.get(contextValue);
|
|
65
|
+
if (httpDetailsList != null) {
|
|
66
|
+
setResult({
|
|
67
|
+
...result,
|
|
68
|
+
extensions: {
|
|
69
|
+
...result.extensions,
|
|
70
|
+
httpDetails: httpDetailsList,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = useIncludeHttpDetailsInExtensions;
|
package/src/secrets.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Parse the yaml secrets string from env to json object
|
|
2
|
+
function loadMeshSecrets(logger, secret) {
|
|
3
|
+
let parsedSecrets = {};
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
// Replace escaped backslashes with a single backslash
|
|
7
|
+
secret = secret.replace(/\\"/g, '"');
|
|
8
|
+
parsedSecrets = JSON.parse(secret);
|
|
9
|
+
} catch (err) {
|
|
10
|
+
logger.error('Error parsing secrets.');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return parsedSecrets;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Custom get secrets handler
|
|
17
|
+
const getSecretsHandler = {
|
|
18
|
+
get: function (target, prop, receiver) {
|
|
19
|
+
if (prop === 'toJSON') {
|
|
20
|
+
// Handle the toJSON case
|
|
21
|
+
return () => target;
|
|
22
|
+
}
|
|
23
|
+
if (prop in target) {
|
|
24
|
+
return Reflect.get(target, prop, receiver);
|
|
25
|
+
} else {
|
|
26
|
+
throw new Error(`The secret ${String(prop)} is not available.`);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
set: function () {
|
|
30
|
+
throw new Error('Setting secrets is not allowed');
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
module.exports = { loadMeshSecrets, getSecretsHandler };
|
package/src/served.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header to indicate which tier served the request.
|
|
3
|
+
*/
|
|
4
|
+
const SERVE_TIER_HEADER = 'x-api-mesh-served';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tiers that served the request.
|
|
8
|
+
*/
|
|
9
|
+
const ServedTier = {
|
|
10
|
+
WORKER_HOT: 0,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Add the served header to the response. Requires mutable headers on the response object.
|
|
15
|
+
* @param response Response.
|
|
16
|
+
* @param servedTier Tier that served the request.
|
|
17
|
+
*/
|
|
18
|
+
const addServedHeader = (response, servedTier) => {
|
|
19
|
+
response.headers.set(SERVE_TIER_HEADER, servedTier.toString());
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
module.exports = { ServedTier, addServedHeader };
|
package/src/server.js
CHANGED
|
@@ -1,200 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
logger.error(err.stack);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// get meshId from command line arguments
|
|
35
|
-
const meshId = process.argv[2];
|
|
36
|
-
|
|
37
|
-
// get PORT number from command line arguments
|
|
38
|
-
const portNo = parseInt(process.argv[3]);
|
|
39
|
-
|
|
40
|
-
const getCORSOptions = () => {
|
|
41
|
-
try {
|
|
42
|
-
const currentWorkingDirectory = process.cwd();
|
|
43
|
-
const meshConfigPath = `${currentWorkingDirectory}/mesh-artifact/${meshId}/.meshrc.json`;
|
|
44
|
-
|
|
45
|
-
const meshConfig = require(meshConfigPath);
|
|
46
|
-
const { responseConfig } = meshConfig;
|
|
47
|
-
const { CORS } = responseConfig;
|
|
48
|
-
|
|
49
|
-
return CORS;
|
|
50
|
-
} catch (e) {
|
|
51
|
-
return {};
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// Custom get secrets handler
|
|
56
|
-
const getSecretsHandler = {
|
|
57
|
-
get: function (target, prop, receiver) {
|
|
58
|
-
if (prop === 'toJSON') {
|
|
59
|
-
// Handle the toJSON case
|
|
60
|
-
return () => target;
|
|
61
|
-
}
|
|
62
|
-
if (prop in target) {
|
|
63
|
-
return Reflect.get(target, prop, receiver);
|
|
64
|
-
} else {
|
|
65
|
-
throw new Error(`The secret ${String(prop)} is not available.`);
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
set: function () {
|
|
69
|
-
throw new Error('Setting secrets is not allowed');
|
|
70
|
-
},
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const { readSecretsFile } = require('./serverUtils');
|
|
3
|
+
|
|
4
|
+
const runServer = (meshId, portNo) => {
|
|
5
|
+
const wranglerPath = `${__dirname}/../node_modules/.bin/wrangler`;
|
|
6
|
+
const indexFilePath = `${__dirname}/index.js`;
|
|
7
|
+
const filePath = '.mesh';
|
|
8
|
+
const secrets = readSecretsFile(filePath);
|
|
9
|
+
const commandArgs = [
|
|
10
|
+
'dev',
|
|
11
|
+
indexFilePath,
|
|
12
|
+
'--var',
|
|
13
|
+
`MESH_ID:${meshId}`,
|
|
14
|
+
`Secret:${JSON.stringify(secrets)}`,
|
|
15
|
+
'--port',
|
|
16
|
+
portNo,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const wrangler = spawn(wranglerPath, commandArgs, {
|
|
20
|
+
stdio: 'inherit',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
wrangler.on('close', code => {
|
|
24
|
+
console.log(`wrangler dev process exited with code ${code}`);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
wrangler.on('error', error => {
|
|
28
|
+
console.error(`Failed to start wrangler dev: ${error.message}`);
|
|
29
|
+
});
|
|
71
30
|
};
|
|
72
31
|
|
|
73
|
-
|
|
74
|
-
if (yogaServer) {
|
|
75
|
-
return yogaServer;
|
|
76
|
-
} else {
|
|
77
|
-
const currentWorkingDirectory = process.cwd();
|
|
78
|
-
const meshArtifactsPath = `${currentWorkingDirectory}/mesh-artifact/${meshId}`;
|
|
79
|
-
|
|
80
|
-
const meshArtifacts = require(meshArtifactsPath);
|
|
81
|
-
const { getBuiltMesh } = meshArtifacts;
|
|
82
|
-
|
|
83
|
-
const tenantMesh = await getBuiltMesh();
|
|
84
|
-
const corsOptions = getCORSOptions();
|
|
85
|
-
|
|
86
|
-
const secrets = readSecretsFile(meshId);
|
|
87
|
-
|
|
88
|
-
const secretsProxy = new Proxy(secrets, getSecretsHandler);
|
|
89
|
-
|
|
90
|
-
logger.info('Creating graphQL server');
|
|
91
|
-
|
|
92
|
-
meshConfig = readMeshConfig(meshId);
|
|
93
|
-
|
|
94
|
-
yogaServer = createYoga({
|
|
95
|
-
plugins: tenantMesh.plugins,
|
|
96
|
-
graphqlEndpoint: `/graphql`,
|
|
97
|
-
graphiql: true,
|
|
98
|
-
maskedErrors: false,
|
|
99
|
-
cors: corsOptions,
|
|
100
|
-
context: initialContext => ({
|
|
101
|
-
...initialContext,
|
|
102
|
-
secrets: secretsProxy,
|
|
103
|
-
}),
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
return yogaServer;
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const app = fastify();
|
|
111
|
-
|
|
112
|
-
app.route({
|
|
113
|
-
method: ['GET', 'POST'],
|
|
114
|
-
url: '/graphql',
|
|
115
|
-
handler: async (req, res) => {
|
|
116
|
-
logger.info('Request received: ', req.body);
|
|
117
|
-
|
|
118
|
-
let body = null;
|
|
119
|
-
let responseBody = null;
|
|
120
|
-
let includeMetaData = false;
|
|
121
|
-
|
|
122
|
-
if (req.headers['x-include-metadata'] && req.headers['x-include-metadata'].length > 0) {
|
|
123
|
-
if (req.headers['x-include-metadata'].toLowerCase() === 'true') {
|
|
124
|
-
includeMetaData = true;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const response = await yogaServer.handleNodeRequest(req, {
|
|
129
|
-
req,
|
|
130
|
-
reply: res,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
try {
|
|
135
|
-
body = await response.text();
|
|
136
|
-
if (body) {
|
|
137
|
-
responseBody = JSON.parse(body);
|
|
138
|
-
}
|
|
139
|
-
} catch (err) {
|
|
140
|
-
logger.error(`Error parsing response body: ${err}`);
|
|
141
|
-
logger.error(response);
|
|
142
|
-
throw new Error(`Error parsing response body: ${err}`);
|
|
143
|
-
}
|
|
144
|
-
//Set the value of includeHTTPDetails flag
|
|
145
|
-
|
|
146
|
-
const includeHTTPDetails = !!meshConfig?.responseConfig?.includeHTTPDetails;
|
|
147
|
-
const meshHTTPDetails = responseBody?.extensions?.httpDetails;
|
|
148
|
-
logger.info('Mesh HTTP Details: ', meshHTTPDetails);
|
|
149
|
-
|
|
150
|
-
/* the logic for handling mesh response headers using includeMetaData */
|
|
151
|
-
prepSourceResponseHeaders(meshHTTPDetails, req.id);
|
|
152
|
-
const responseHeaders = processResponseHeaders(meshId, req.id, includeMetaData, req.method);
|
|
153
|
-
|
|
154
|
-
/** Adding the yoga response headers to the response */
|
|
155
|
-
response.headers?.forEach((value, key) => {
|
|
156
|
-
res.header(key, value);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Delete the httpDetails extensions details if mesh owner has disabled those details in the config
|
|
160
|
-
if (includeHTTPDetails !== true) {
|
|
161
|
-
delete responseBody?.extensions?.httpDetails;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
//make sure to remove the request headers from cache after the request is complete
|
|
165
|
-
removeRequestHeaders(req.id);
|
|
166
|
-
const fastifyResponseBody = JSON.stringify(responseBody);
|
|
167
|
-
res.status(response.status).headers(responseHeaders).send(fastifyResponseBody);
|
|
168
|
-
} catch (err) {
|
|
169
|
-
logger.error(`Error parsing response body: ${err}`);
|
|
170
|
-
//we have this fallback catch clause if someone wants to load the graphiql engine. This returns the default headers back
|
|
171
|
-
response.headers?.forEach((value, key) => {
|
|
172
|
-
res.header(key, value);
|
|
173
|
-
});
|
|
174
|
-
res.status(response.status);
|
|
175
|
-
res.send(response.body);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return res;
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
app.listen(
|
|
183
|
-
{
|
|
184
|
-
//set the port no of the server based on the input value
|
|
185
|
-
port: portNo,
|
|
186
|
-
},
|
|
187
|
-
async err => {
|
|
188
|
-
try {
|
|
189
|
-
if (err) {
|
|
190
|
-
throw new Error(`Server setup error: ${err.message}`);
|
|
191
|
-
}
|
|
192
|
-
yogaServer = await getYogaServer();
|
|
193
|
-
} catch (error) {
|
|
194
|
-
console.error(error);
|
|
195
|
-
process.exit(1);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
console.log(`Server is running on http://localhost:${portNo}/graphql`);
|
|
199
|
-
},
|
|
200
|
-
);
|
|
32
|
+
module.exports = { runServer };
|
package/src/serverUtils.js
CHANGED
|
@@ -318,13 +318,13 @@ function ccDirectivesToString(directives) {
|
|
|
318
318
|
|
|
319
319
|
/**
|
|
320
320
|
* Returns secrets content from artifacts
|
|
321
|
-
* @param
|
|
321
|
+
* @param meshPath
|
|
322
322
|
* @returns
|
|
323
323
|
*/
|
|
324
|
-
function readSecretsFile(
|
|
324
|
+
function readSecretsFile(meshPath) {
|
|
325
325
|
let secrets = {};
|
|
326
326
|
try {
|
|
327
|
-
const filePath = path.resolve(process.cwd(),
|
|
327
|
+
const filePath = path.resolve(process.cwd(), `${meshPath}`, 'secrets.yaml');
|
|
328
328
|
if (fs.existsSync(filePath)) {
|
|
329
329
|
secrets = YAML.parse(fs.readFileSync(filePath, 'utf8'));
|
|
330
330
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { getMesh } from '@graphql-mesh/runtime';
|
|
2
|
+
|
|
3
|
+
const { getCorsOptions } = require('./cors');
|
|
4
|
+
const { createYoga } = require('graphql-yoga');
|
|
5
|
+
const { GraphQLError } = require('graphql/error');
|
|
6
|
+
|
|
7
|
+
const { loadMeshSecrets, getSecretsHandler } = require('./secrets');
|
|
8
|
+
const useComplianceHeaders = require('./plugins/complianceHeaders');
|
|
9
|
+
const UseHttpDetailsExtensions = require('./plugins/httpDetailsExtensions');
|
|
10
|
+
const useSourceHeaders = require('@adobe/plugin-source-headers');
|
|
11
|
+
|
|
12
|
+
let meshInstance$;
|
|
13
|
+
|
|
14
|
+
async function buildMeshInstance(meshArtifacts, meshConfig) {
|
|
15
|
+
const { getMeshOptions } = meshArtifacts;
|
|
16
|
+
const options = await getMeshOptions();
|
|
17
|
+
|
|
18
|
+
options.additionalEnvelopPlugins = (options.additionalEnvelopPlugins || []).concat(
|
|
19
|
+
useComplianceHeaders(),
|
|
20
|
+
UseHttpDetailsExtensions({
|
|
21
|
+
// Get the details of responseConfig.includeHTTPDetails and store in Cache
|
|
22
|
+
if: meshConfig.responseConfig?.includeHTTPDetails || false,
|
|
23
|
+
}),
|
|
24
|
+
useSourceHeaders(meshConfig),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return getMesh(options).then(mesh => {
|
|
28
|
+
const id = mesh.pubsub.subscribe('destroy', () => {
|
|
29
|
+
meshInstance$ = undefined;
|
|
30
|
+
mesh.pubsub.unsubscribe(id);
|
|
31
|
+
});
|
|
32
|
+
return mesh;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getBuiltMesh(meshArtifacts, meshConfig) {
|
|
37
|
+
if (meshInstance$ == null) {
|
|
38
|
+
meshInstance$ = buildMeshInstance(meshArtifacts, meshConfig);
|
|
39
|
+
}
|
|
40
|
+
return meshInstance$;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const buildServer = async (loggerInstance, env, meshArtifacts, meshConfig) => {
|
|
44
|
+
const { MESH_ID: meshId, Secret: secret } = env;
|
|
45
|
+
const tenantMesh = await getBuiltMesh(meshArtifacts, meshConfig);
|
|
46
|
+
const meshSecrets = loadMeshSecrets(loggerInstance, secret);
|
|
47
|
+
return await buildYogaServer(env, tenantMesh, meshId, meshConfig, meshSecrets);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
async function buildYogaServer(env, tenantMesh, meshId, meshConfig, meshSecrets) {
|
|
51
|
+
const secretsProxy = new Proxy(meshSecrets, getSecretsHandler);
|
|
52
|
+
return createYoga({
|
|
53
|
+
plugins: tenantMesh.plugins,
|
|
54
|
+
graphqlEndpoint: `/api/${meshId}/graphql`,
|
|
55
|
+
cors: getCorsOptions(env, meshConfig),
|
|
56
|
+
context: initialContext => ({
|
|
57
|
+
...initialContext,
|
|
58
|
+
secrets: secretsProxy,
|
|
59
|
+
}),
|
|
60
|
+
maskedErrors: {
|
|
61
|
+
maskError: maskError,
|
|
62
|
+
},
|
|
63
|
+
logging: 'debug',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const maskError = error => {
|
|
68
|
+
if (error instanceof GraphQLError && error.extensions?.http?.headers) {
|
|
69
|
+
delete error.extensions.http.headers;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return error;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
module.exports = { buildServer };
|