@friggframework/devtools 2.0.0-next.0 → 2.0.0-next.10
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/frigg-cli/build-command/index.js +33 -0
- package/frigg-cli/deploy-command/index.js +33 -0
- package/frigg-cli/index.js +23 -2
- package/frigg-cli/index.test.js +87 -35
- package/frigg-cli/{environmentVariables.js → install-command/environment-variables.js} +11 -18
- package/frigg-cli/install-command/environment-variables.test.js +136 -0
- package/frigg-cli/{installCommand.js → install-command/index.js} +7 -7
- package/frigg-cli/{validatePackage.js → install-command/validate-package.js} +9 -13
- package/frigg-cli/start-command/index.js +36 -0
- package/index.js +4 -2
- package/infrastructure/create-frigg-infrastructure.js +38 -0
- package/infrastructure/index.js +4 -0
- package/infrastructure/serverless-template.js +283 -0
- package/infrastructure/webpack.config.js +20 -0
- package/package.json +18 -7
- package/test/mock-integration.js +61 -56
- package/frigg-cli/environmentVariables.test.js +0 -86
- /package/frigg-cli/{backendJs.js → install-command/backend-js.js} +0 -0
- /package/frigg-cli/{commitChanges.js → install-command/commit-changes.js} +0 -0
- /package/frigg-cli/{installPackage.js → install-command/install-package.js} +0 -0
- /package/frigg-cli/{integrationFile.js → install-command/integration-file.js} +0 -0
- /package/frigg-cli/{logger.js → install-command/logger.js} +0 -0
- /package/frigg-cli/{template.js → install-command/template.js} +0 -0
- /package/frigg-cli/{backendPath.js → utils/backend-path.js} +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function buildCommand(options) {
|
|
5
|
+
console.log('Building the serverless application...');
|
|
6
|
+
const backendPath = path.resolve(process.cwd());
|
|
7
|
+
const infrastructurePath = 'infrastructure.js';
|
|
8
|
+
const command = 'serverless';
|
|
9
|
+
const serverlessArgs = [
|
|
10
|
+
'package',
|
|
11
|
+
'--config',
|
|
12
|
+
infrastructurePath,
|
|
13
|
+
'--stage',
|
|
14
|
+
options.stage
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const childProcess = spawn(command, serverlessArgs, {
|
|
18
|
+
cwd: backendPath,
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
childProcess.on('error', (error) => {
|
|
23
|
+
console.error(`Error executing command: ${error.message}`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
childProcess.on('close', (code) => {
|
|
27
|
+
if (code !== 0) {
|
|
28
|
+
console.log(`Child process exited with code ${code}`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { buildCommand };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function deployCommand(options) {
|
|
5
|
+
console.log('Deploying the serverless application...');
|
|
6
|
+
const backendPath = path.resolve(process.cwd());
|
|
7
|
+
const infrastructurePath = 'infrastructure.js';
|
|
8
|
+
const command = 'serverless';
|
|
9
|
+
const serverlessArgs = [
|
|
10
|
+
'deploy',
|
|
11
|
+
'--config',
|
|
12
|
+
infrastructurePath,
|
|
13
|
+
'--stage',
|
|
14
|
+
options.stage
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const childProcess = spawn(command, serverlessArgs, {
|
|
18
|
+
cwd: backendPath,
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
childProcess.on('error', (error) => {
|
|
23
|
+
console.error(`Error executing command: ${error.message}`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
childProcess.on('close', (code) => {
|
|
27
|
+
if (code !== 0) {
|
|
28
|
+
console.log(`Child process exited with code ${code}`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { deployCommand };
|
package/frigg-cli/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { Command } = require('commander');
|
|
4
|
-
const { installCommand } = require('./
|
|
4
|
+
const { installCommand } = require('./install-command');
|
|
5
|
+
const { startCommand } = require('./start-command'); // Assuming you have a startCommand module
|
|
6
|
+
const { buildCommand } = require('./build-command');
|
|
7
|
+
const { deployCommand } = require('./deploy-command');
|
|
5
8
|
|
|
6
9
|
const program = new Command();
|
|
7
10
|
program
|
|
@@ -9,6 +12,24 @@ program
|
|
|
9
12
|
.description('Install an API module')
|
|
10
13
|
.action(installCommand);
|
|
11
14
|
|
|
15
|
+
program
|
|
16
|
+
.command('start')
|
|
17
|
+
.description('Run the backend and optional frontend')
|
|
18
|
+
.option('-s, --stage <stage>', 'deployment stage', 'dev')
|
|
19
|
+
.action(startCommand);
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.command('build')
|
|
23
|
+
.description('Build the serverless application')
|
|
24
|
+
.option('-s, --stage <stage>', 'deployment stage', 'dev')
|
|
25
|
+
.action(buildCommand);
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('deploy')
|
|
29
|
+
.description('Deploy the serverless application')
|
|
30
|
+
.option('-s, --stage <stage>', 'deployment stage', 'dev')
|
|
31
|
+
.action(deployCommand);
|
|
32
|
+
|
|
12
33
|
program.parse(process.argv);
|
|
13
34
|
|
|
14
|
-
module.exports = { installCommand };
|
|
35
|
+
module.exports = { installCommand, startCommand, buildCommand, deployCommand };
|
package/frigg-cli/index.test.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
2
|
const { installCommand } = require('./index');
|
|
3
|
-
const { validatePackageExists } = require('./
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const {
|
|
9
|
-
const {
|
|
3
|
+
const { validatePackageExists } = require('./install-command/validate-package');
|
|
4
|
+
const {
|
|
5
|
+
findNearestBackendPackageJson,
|
|
6
|
+
validateBackendPath,
|
|
7
|
+
} = require('./utils/backend-path');
|
|
8
|
+
const { installPackage } = require('./install-command/install-package');
|
|
9
|
+
const { createIntegrationFile } = require('./install-command/integration-file');
|
|
10
|
+
const { updateBackendJsFile } = require('./install-command/backend-js');
|
|
11
|
+
const { commitChanges } = require('./install-command/commit-changes');
|
|
12
|
+
const { logInfo, logError } = require('./install-command/logger');
|
|
10
13
|
|
|
11
14
|
describe('CLI Command Tests', () => {
|
|
12
15
|
it('should successfully install an API module when all steps complete without errors', async () => {
|
|
@@ -14,26 +17,28 @@ describe('CLI Command Tests', () => {
|
|
|
14
17
|
const mockPackageName = `@friggframework/api-module-${mockApiModuleName}`;
|
|
15
18
|
const mockBackendPath = '/mock/backend/path';
|
|
16
19
|
|
|
17
|
-
jest.mock('./
|
|
20
|
+
jest.mock('./install-command/validate-package', () => ({
|
|
18
21
|
validatePackageExists: jest.fn().mockResolvedValue(true),
|
|
19
22
|
}));
|
|
20
|
-
jest.mock('./
|
|
21
|
-
findNearestBackendPackageJson: jest
|
|
23
|
+
jest.mock('./utils/backend-path', () => ({
|
|
24
|
+
findNearestBackendPackageJson: jest
|
|
25
|
+
.fn()
|
|
26
|
+
.mockReturnValue(mockBackendPath),
|
|
22
27
|
validateBackendPath: jest.fn().mockReturnValue(true),
|
|
23
28
|
}));
|
|
24
|
-
jest.mock('./
|
|
29
|
+
jest.mock('./install-command/install-package', () => ({
|
|
25
30
|
installPackage: jest.fn().mockReturnValue(true),
|
|
26
31
|
}));
|
|
27
|
-
jest.mock('./
|
|
32
|
+
jest.mock('./install-command/integration-file', () => ({
|
|
28
33
|
createIntegrationFile: jest.fn().mockReturnValue(true),
|
|
29
34
|
}));
|
|
30
|
-
jest.mock('./
|
|
35
|
+
jest.mock('./install-command/backend-js', () => ({
|
|
31
36
|
updateBackendJsFile: jest.fn().mockReturnValue(true),
|
|
32
37
|
}));
|
|
33
|
-
jest.mock('./
|
|
38
|
+
jest.mock('./install-command/commit-changes', () => ({
|
|
34
39
|
commitChanges: jest.fn().mockReturnValue(true),
|
|
35
40
|
}));
|
|
36
|
-
jest.mock('./logger', () => ({
|
|
41
|
+
jest.mock('./install-command/logger', () => ({
|
|
37
42
|
logInfo: jest.fn(),
|
|
38
43
|
logError: jest.fn(),
|
|
39
44
|
}));
|
|
@@ -49,19 +54,42 @@ describe('CLI Command Tests', () => {
|
|
|
49
54
|
expect(validatePackageExists).toHaveBeenCalledWith(mockPackageName);
|
|
50
55
|
expect(findNearestBackendPackageJson).toHaveBeenCalled();
|
|
51
56
|
expect(validateBackendPath).toHaveBeenCalledWith(mockBackendPath);
|
|
52
|
-
expect(installPackage).toHaveBeenCalledWith(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
expect(
|
|
57
|
+
expect(installPackage).toHaveBeenCalledWith(
|
|
58
|
+
mockBackendPath,
|
|
59
|
+
mockPackageName
|
|
60
|
+
);
|
|
61
|
+
expect(createIntegrationFile).toHaveBeenCalledWith(
|
|
62
|
+
mockBackendPath,
|
|
63
|
+
mockApiModuleName
|
|
64
|
+
);
|
|
65
|
+
expect(updateBackendJsFile).toHaveBeenCalledWith(
|
|
66
|
+
mockBackendPath,
|
|
67
|
+
mockApiModuleName
|
|
68
|
+
);
|
|
69
|
+
expect(commitChanges).toHaveBeenCalledWith(
|
|
70
|
+
mockBackendPath,
|
|
71
|
+
mockApiModuleName
|
|
72
|
+
);
|
|
73
|
+
expect(logInfo).toHaveBeenCalledWith(
|
|
74
|
+
`Successfully installed ${mockPackageName} and updated the project.`
|
|
75
|
+
);
|
|
57
76
|
});
|
|
58
77
|
|
|
59
78
|
it('should log an error and exit with code 1 if the package does not exist', async () => {
|
|
60
|
-
const mockExit = jest
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
const mockExit = jest
|
|
80
|
+
.spyOn(process, 'exit')
|
|
81
|
+
.mockImplementation(() => {});
|
|
82
|
+
const mockLogError = jest
|
|
83
|
+
.spyOn(require('./install-command/logger'), 'logError')
|
|
84
|
+
.mockImplementation(() => {});
|
|
85
|
+
const mockValidatePackageExists = jest
|
|
86
|
+
.spyOn(
|
|
87
|
+
require('./install-command/validate-package'),
|
|
88
|
+
'validatePackageExists'
|
|
89
|
+
)
|
|
90
|
+
.mockImplementation(() => {
|
|
91
|
+
throw new Error('Package not found');
|
|
92
|
+
});
|
|
65
93
|
|
|
66
94
|
const program = new Command();
|
|
67
95
|
program
|
|
@@ -71,8 +99,13 @@ describe('CLI Command Tests', () => {
|
|
|
71
99
|
|
|
72
100
|
await program.parseAsync(['node', 'install', 'nonexistent-package']);
|
|
73
101
|
|
|
74
|
-
expect(mockValidatePackageExists).toHaveBeenCalledWith(
|
|
75
|
-
|
|
102
|
+
expect(mockValidatePackageExists).toHaveBeenCalledWith(
|
|
103
|
+
'@friggframework/api-module-nonexistent-package'
|
|
104
|
+
);
|
|
105
|
+
expect(mockLogError).toHaveBeenCalledWith(
|
|
106
|
+
'An error occurred:',
|
|
107
|
+
expect.any(Error)
|
|
108
|
+
);
|
|
76
109
|
expect(mockExit).toHaveBeenCalledWith(1);
|
|
77
110
|
|
|
78
111
|
mockExit.mockRestore();
|
|
@@ -81,13 +114,29 @@ describe('CLI Command Tests', () => {
|
|
|
81
114
|
});
|
|
82
115
|
|
|
83
116
|
it('should log an error and exit with code 1 if the backend path is invalid', async () => {
|
|
84
|
-
const mockLogError = jest
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
const mockLogError = jest
|
|
118
|
+
.spyOn(require('./install-command/logger'), 'logError')
|
|
119
|
+
.mockImplementation(() => {});
|
|
120
|
+
const mockProcessExit = jest
|
|
121
|
+
.spyOn(process, 'exit')
|
|
122
|
+
.mockImplementation(() => {});
|
|
123
|
+
const mockValidatePackageExists = jest
|
|
124
|
+
.spyOn(
|
|
125
|
+
require('./install-command/validate-package'),
|
|
126
|
+
'validatePackageExists'
|
|
127
|
+
)
|
|
128
|
+
.mockResolvedValue(true);
|
|
129
|
+
const mockFindNearestBackendPackageJson = jest
|
|
130
|
+
.spyOn(
|
|
131
|
+
require('./utils/backend-path'),
|
|
132
|
+
'findNearestBackendPackageJson'
|
|
133
|
+
)
|
|
134
|
+
.mockReturnValue('/invalid/path');
|
|
135
|
+
const mockValidateBackendPath = jest
|
|
136
|
+
.spyOn(require('./utils/backend-path'), 'validateBackendPath')
|
|
137
|
+
.mockImplementation(() => {
|
|
138
|
+
throw new Error('Invalid backend path');
|
|
139
|
+
});
|
|
91
140
|
|
|
92
141
|
const program = new Command();
|
|
93
142
|
program
|
|
@@ -97,7 +146,10 @@ describe('CLI Command Tests', () => {
|
|
|
97
146
|
|
|
98
147
|
await program.parseAsync(['node', 'install', 'test-module']);
|
|
99
148
|
|
|
100
|
-
expect(mockLogError).toHaveBeenCalledWith(
|
|
149
|
+
expect(mockLogError).toHaveBeenCalledWith(
|
|
150
|
+
'An error occurred:',
|
|
151
|
+
expect.any(Error)
|
|
152
|
+
);
|
|
101
153
|
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
|
102
154
|
|
|
103
155
|
mockLogError.mockRestore();
|
|
@@ -3,8 +3,7 @@ const dotenv = require('dotenv');
|
|
|
3
3
|
const { readFileSync, writeFileSync, existsSync } = require('fs');
|
|
4
4
|
const { logInfo } = require('./logger');
|
|
5
5
|
const { resolve } = require('node:path');
|
|
6
|
-
const
|
|
7
|
-
|
|
6
|
+
const { confirm, input } = require('@inquirer/prompts');
|
|
8
7
|
const { parse } = require('@babel/parser');
|
|
9
8
|
const traverse = require('@babel/traverse').default;
|
|
10
9
|
|
|
@@ -82,26 +81,20 @@ const handleEnvVariables = async (backendPath, modulePath) => {
|
|
|
82
81
|
logInfo(`Missing environment variables: ${missingEnvVars.join(', ')}`);
|
|
83
82
|
|
|
84
83
|
if (missingEnvVars.length > 0) {
|
|
85
|
-
const
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
', '
|
|
91
|
-
)}. Do you want to add them now?`,
|
|
92
|
-
},
|
|
93
|
-
]);
|
|
84
|
+
const addEnvVars = await confirm({
|
|
85
|
+
message: `The following environment variables are required: ${missingEnvVars.join(
|
|
86
|
+
', '
|
|
87
|
+
)}. Do you want to add them now?`,
|
|
88
|
+
});
|
|
94
89
|
|
|
95
90
|
if (addEnvVars) {
|
|
96
91
|
const envValues = {};
|
|
97
92
|
for (const envVar of missingEnvVars) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
},
|
|
104
|
-
]);
|
|
93
|
+
const value = await input({
|
|
94
|
+
type: 'input',
|
|
95
|
+
name: 'value',
|
|
96
|
+
message: `Enter value for ${envVar}:`,
|
|
97
|
+
});
|
|
105
98
|
envValues[envVar] = value;
|
|
106
99
|
}
|
|
107
100
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const { handleEnvVariables } = require('./environment-variables');
|
|
2
|
+
const { logInfo } = require('./logger');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const dotenv = require('dotenv');
|
|
6
|
+
const { resolve } = require('node:path');
|
|
7
|
+
const { parse } = require('@babel/parser');
|
|
8
|
+
const traverse = require('@babel/traverse');
|
|
9
|
+
|
|
10
|
+
jest.mock('inquirer');
|
|
11
|
+
jest.mock('fs');
|
|
12
|
+
jest.mock('dotenv');
|
|
13
|
+
jest.mock('./logger');
|
|
14
|
+
jest.mock('@babel/parser');
|
|
15
|
+
jest.mock('@babel/traverse');
|
|
16
|
+
|
|
17
|
+
describe('handleEnvVariables', () => {
|
|
18
|
+
const backendPath = '/mock/backend/path';
|
|
19
|
+
const modulePath = '/mock/module/path';
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
fs.readFileSync.mockReturnValue(`
|
|
24
|
+
const Definition = {
|
|
25
|
+
env: {
|
|
26
|
+
client_id: process.env.GOOGLE_CALENDAR_CLIENT_ID,
|
|
27
|
+
client_secret: process.env.GOOGLE_CALENDAR_CLIENT_SECRET,
|
|
28
|
+
redirect_uri: \`\${process.env.REDIRECT_URI}/google-calendar\`,
|
|
29
|
+
scope: process.env.GOOGLE_CALENDAR_SCOPE,
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
`);
|
|
33
|
+
parse.mockReturnValue({});
|
|
34
|
+
traverse.default.mockImplementation((ast, visitor) => {
|
|
35
|
+
visitor.ObjectProperty({
|
|
36
|
+
node: {
|
|
37
|
+
key: { name: 'env' },
|
|
38
|
+
value: {
|
|
39
|
+
properties: [
|
|
40
|
+
{
|
|
41
|
+
key: { name: 'client_id' },
|
|
42
|
+
value: {
|
|
43
|
+
type: 'MemberExpression',
|
|
44
|
+
object: { name: 'process' },
|
|
45
|
+
property: {
|
|
46
|
+
name: 'GOOGLE_CALENDAR_CLIENT_ID',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: { name: 'client_secret' },
|
|
52
|
+
value: {
|
|
53
|
+
type: 'MemberExpression',
|
|
54
|
+
object: { name: 'process' },
|
|
55
|
+
property: {
|
|
56
|
+
name: 'GOOGLE_CALENDAR_CLIENT_SECRET',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: { name: 'redirect_uri' },
|
|
62
|
+
value: {
|
|
63
|
+
type: 'MemberExpression',
|
|
64
|
+
object: { name: 'process' },
|
|
65
|
+
property: { name: 'REDIRECT_URI' },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: { name: 'scope' },
|
|
70
|
+
value: {
|
|
71
|
+
type: 'MemberExpression',
|
|
72
|
+
object: { name: 'process' },
|
|
73
|
+
property: { name: 'GOOGLE_CALENDAR_SCOPE' },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
xit('should identify and handle missing environment variables', async () => {
|
|
84
|
+
const localEnvPath = resolve(backendPath, '../.env');
|
|
85
|
+
const localDevConfigPath = resolve(
|
|
86
|
+
backendPath,
|
|
87
|
+
'../src/configs/dev.json'
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
fs.existsSync.mockImplementation(
|
|
91
|
+
(path) => path === localEnvPath || path === localDevConfigPath
|
|
92
|
+
);
|
|
93
|
+
dotenv.parse.mockReturnValue({});
|
|
94
|
+
fs.readFileSync.mockImplementation((path) => {
|
|
95
|
+
if (path === resolve(modulePath, 'index.js'))
|
|
96
|
+
return 'mock module content';
|
|
97
|
+
if (path === localEnvPath) return '';
|
|
98
|
+
if (path === localDevConfigPath) return '{}';
|
|
99
|
+
return '';
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
inquirer.prompt
|
|
103
|
+
.mockResolvedValueOnce({ addEnvVars: true })
|
|
104
|
+
.mockResolvedValueOnce({ value: 'client_id_value' })
|
|
105
|
+
.mockResolvedValueOnce({ value: 'client_secret_value' })
|
|
106
|
+
.mockResolvedValueOnce({ value: 'redirect_uri_value' })
|
|
107
|
+
.mockResolvedValueOnce({ value: 'scope_value' });
|
|
108
|
+
|
|
109
|
+
await handleEnvVariables(backendPath, modulePath);
|
|
110
|
+
|
|
111
|
+
expect(logInfo).toHaveBeenCalledWith(
|
|
112
|
+
'Searching for missing environment variables...'
|
|
113
|
+
);
|
|
114
|
+
expect(logInfo).toHaveBeenCalledWith(
|
|
115
|
+
'Missing environment variables: GOOGLE_CALENDAR_CLIENT_ID, GOOGLE_CALENDAR_CLIENT_SECRET, REDIRECT_URI, GOOGLE_CALENDAR_SCOPE'
|
|
116
|
+
);
|
|
117
|
+
expect(inquirer.prompt).toHaveBeenCalledTimes(5);
|
|
118
|
+
expect(fs.appendFileSync).toHaveBeenCalledWith(
|
|
119
|
+
localEnvPath,
|
|
120
|
+
'\nGOOGLE_CALENDAR_CLIENT_ID=client_id_value\nGOOGLE_CALENDAR_CLIENT_SECRET=client_secret_value\nREDIRECT_URI=redirect_uri_value\nGOOGLE_CALENDAR_SCOPE=scope_value'
|
|
121
|
+
);
|
|
122
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
123
|
+
localDevConfigPath,
|
|
124
|
+
JSON.stringify(
|
|
125
|
+
{
|
|
126
|
+
GOOGLE_CALENDAR_CLIENT_ID: 'client_id_value',
|
|
127
|
+
GOOGLE_CALENDAR_CLIENT_SECRET: 'client_secret_value',
|
|
128
|
+
REDIRECT_URI: 'redirect_uri_value',
|
|
129
|
+
GOOGLE_CALENDAR_SCOPE: 'scope_value',
|
|
130
|
+
},
|
|
131
|
+
null,
|
|
132
|
+
2
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
const { installPackage } = require('./
|
|
2
|
-
const { createIntegrationFile } = require('./
|
|
1
|
+
const { installPackage } = require('./install-package');
|
|
2
|
+
const { createIntegrationFile } = require('./integration-file');
|
|
3
3
|
const { resolve } = require('node:path');
|
|
4
|
-
const { updateBackendJsFile } = require('./
|
|
4
|
+
const { updateBackendJsFile } = require('./backend-js');
|
|
5
5
|
const { logInfo, logError } = require('./logger');
|
|
6
|
-
const { commitChanges } = require('./
|
|
6
|
+
const { commitChanges } = require('./commit-changes');
|
|
7
7
|
const {
|
|
8
8
|
findNearestBackendPackageJson,
|
|
9
9
|
validateBackendPath,
|
|
10
|
-
} = require('
|
|
11
|
-
const { handleEnvVariables } = require('./
|
|
10
|
+
} = require('../utils/backend-path');
|
|
11
|
+
const { handleEnvVariables } = require('./environment-variables');
|
|
12
12
|
const {
|
|
13
13
|
validatePackageExists,
|
|
14
14
|
searchAndSelectPackage,
|
|
15
|
-
} = require('./
|
|
15
|
+
} = require('./validate-package');
|
|
16
16
|
|
|
17
17
|
const installCommand = async (apiModuleName) => {
|
|
18
18
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { execSync } = require('child_process');
|
|
2
2
|
const axios = require('axios');
|
|
3
3
|
const { logError } = require('./logger');
|
|
4
|
-
const
|
|
4
|
+
const { checkbox } = require('@inquirer/prompts');
|
|
5
5
|
|
|
6
6
|
async function searchPackages(apiModuleName) {
|
|
7
7
|
const searchCommand = `npm search @friggframework/api-module-${apiModuleName} --json`;
|
|
@@ -36,9 +36,7 @@ const searchAndSelectPackage = async (apiModuleName) => {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const filteredResults = searchResults.filter((pkg) => {
|
|
39
|
-
const version = pkg.version
|
|
40
|
-
? pkg.version.split('.').map(Number)
|
|
41
|
-
: [];
|
|
39
|
+
const version = pkg.version ? pkg.version.split('.').map(Number) : [];
|
|
42
40
|
return version[0] >= 1;
|
|
43
41
|
});
|
|
44
42
|
|
|
@@ -55,20 +53,18 @@ const searchAndSelectPackage = async (apiModuleName) => {
|
|
|
55
53
|
const choices = filteredResults.map((pkg) => {
|
|
56
54
|
return {
|
|
57
55
|
name: `${pkg.name} (${pkg.version})`,
|
|
56
|
+
value: pkg.name,
|
|
58
57
|
checked: filteredResults.length === 1, // Automatically select if only one result
|
|
59
58
|
};
|
|
60
59
|
});
|
|
61
60
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
choices,
|
|
68
|
-
},
|
|
69
|
-
]);
|
|
61
|
+
const selectedPackages = await checkbox({
|
|
62
|
+
message: 'Select the packages to install:',
|
|
63
|
+
choices,
|
|
64
|
+
});
|
|
65
|
+
console.log('Selected packages:', selectedPackages);
|
|
70
66
|
|
|
71
|
-
return selectedPackages.map(choice => choice.split(' ')[0]);
|
|
67
|
+
return selectedPackages.map((choice) => choice.split(' ')[0]);
|
|
72
68
|
};
|
|
73
69
|
|
|
74
70
|
module.exports = {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function startCommand(options) {
|
|
5
|
+
console.log('Starting backend and optional frontend...');
|
|
6
|
+
// Suppress AWS SDK warning message about maintenance mode
|
|
7
|
+
process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE = 1;
|
|
8
|
+
const backendPath = path.resolve(process.cwd());
|
|
9
|
+
console.log(`Starting backend in ${backendPath}...`);
|
|
10
|
+
const infrastructurePath = 'infrastructure.js';
|
|
11
|
+
const command = 'serverless';
|
|
12
|
+
const args = [
|
|
13
|
+
'offline',
|
|
14
|
+
'--config',
|
|
15
|
+
infrastructurePath,
|
|
16
|
+
'--stage',
|
|
17
|
+
options.stage
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const childProcess = spawn(command, args, {
|
|
21
|
+
cwd: backendPath,
|
|
22
|
+
stdio: 'inherit',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
childProcess.on('error', (error) => {
|
|
26
|
+
console.error(`Error executing command: ${error.message}`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
childProcess.on('close', (code) => {
|
|
30
|
+
if (code !== 0) {
|
|
31
|
+
console.log(`Child process exited with code ${code}`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { startCommand };
|
package/index.js
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { composeServerlessDefinition } = require('./serverless-template');
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
findNearestBackendPackageJson,
|
|
7
|
+
} = require('../frigg-cli/utils/backend-path');
|
|
8
|
+
|
|
9
|
+
function createFriggInfrastructure() {
|
|
10
|
+
const backendPath = findNearestBackendPackageJson();
|
|
11
|
+
if (!backendPath) {
|
|
12
|
+
throw new Error('Could not find backend package.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const backendDir = path.dirname(backendPath);
|
|
16
|
+
const backendFilePath = path.join(backendDir, 'index.js');
|
|
17
|
+
if (!fs.existsSync(backendFilePath)) {
|
|
18
|
+
throw new Error('Could not find index.js');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const backend = require(backendFilePath);
|
|
22
|
+
const appDefinition = backend.Definition;
|
|
23
|
+
|
|
24
|
+
// const serverlessTemplate = require(path.resolve(
|
|
25
|
+
// __dirname,
|
|
26
|
+
// './serverless-template.js'
|
|
27
|
+
// ));
|
|
28
|
+
const definition = composeServerlessDefinition(
|
|
29
|
+
appDefinition,
|
|
30
|
+
backend.IntegrationFactory
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
...definition,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { createFriggInfrastructure };
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
const composeServerlessDefinition = (AppDefinition) => {
|
|
5
|
+
const definition = {
|
|
6
|
+
frameworkVersion: '>=3.17.0',
|
|
7
|
+
service: AppDefinition.name || 'create-frigg-app',
|
|
8
|
+
package: {
|
|
9
|
+
individually: true,
|
|
10
|
+
},
|
|
11
|
+
useDotenv: true,
|
|
12
|
+
provider: {
|
|
13
|
+
name: AppDefinition.provider || 'aws',
|
|
14
|
+
runtime: 'nodejs20.x',
|
|
15
|
+
timeout: 30,
|
|
16
|
+
region: 'us-east-1',
|
|
17
|
+
stage: '${opt:stage}',
|
|
18
|
+
environment: {
|
|
19
|
+
STAGE: '${opt:stage}',
|
|
20
|
+
AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
|
|
21
|
+
},
|
|
22
|
+
iamRoleStatements: [
|
|
23
|
+
{
|
|
24
|
+
Effect: 'Allow',
|
|
25
|
+
Action: ['sns:Publish'],
|
|
26
|
+
Resource: {
|
|
27
|
+
Ref: 'InternalErrorBridgeTopic',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
plugins: [
|
|
33
|
+
// 'serverless-webpack',
|
|
34
|
+
'serverless-dotenv-plugin',
|
|
35
|
+
'serverless-offline-sqs',
|
|
36
|
+
'serverless-offline',
|
|
37
|
+
'@friggframework/serverless-plugin',
|
|
38
|
+
],
|
|
39
|
+
custom: {
|
|
40
|
+
'serverless-offline': {
|
|
41
|
+
httpPort: 3001,
|
|
42
|
+
lambdaPort: 4001,
|
|
43
|
+
websocketPort: 3002,
|
|
44
|
+
},
|
|
45
|
+
'serverless-offline-sqs': {
|
|
46
|
+
autoCreate: false,
|
|
47
|
+
apiVersion: '2012-11-05',
|
|
48
|
+
endpoint: 'http://localhost:4566',
|
|
49
|
+
region: 'us-east-1',
|
|
50
|
+
accessKeyId: 'root',
|
|
51
|
+
secretAccessKey: 'root',
|
|
52
|
+
skipCacheInvalidation: false,
|
|
53
|
+
},
|
|
54
|
+
webpack: {
|
|
55
|
+
webpackConfig: 'webpack.config.js',
|
|
56
|
+
includeModules: {
|
|
57
|
+
forceExclude: ['aws-sdk'],
|
|
58
|
+
},
|
|
59
|
+
packager: 'npm',
|
|
60
|
+
excludeFiles: ['src/**/*.test.js', 'test/'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
functions: {
|
|
64
|
+
defaultWebsocket: {
|
|
65
|
+
handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
|
|
66
|
+
events: [
|
|
67
|
+
{
|
|
68
|
+
websocket: {
|
|
69
|
+
route: '$connect',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
websocket: {
|
|
74
|
+
route: '$default',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
websocket: {
|
|
79
|
+
route: '$disconnect',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
auth: {
|
|
85
|
+
handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
|
|
86
|
+
events: [
|
|
87
|
+
{
|
|
88
|
+
http: {
|
|
89
|
+
path: '/api/integrations',
|
|
90
|
+
method: 'ANY',
|
|
91
|
+
cors: true,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
http: {
|
|
96
|
+
path: '/api/integrations/{proxy+}',
|
|
97
|
+
method: 'ANY',
|
|
98
|
+
cors: true,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
http: {
|
|
103
|
+
path: '/api/authorize',
|
|
104
|
+
method: 'ANY',
|
|
105
|
+
cors: true,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
user: {
|
|
111
|
+
handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
|
|
112
|
+
events: [
|
|
113
|
+
{
|
|
114
|
+
http: {
|
|
115
|
+
path: '/user/{proxy+}',
|
|
116
|
+
method: 'ANY',
|
|
117
|
+
cors: true,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
resources: {
|
|
124
|
+
Resources: {
|
|
125
|
+
InternalErrorQueue: {
|
|
126
|
+
Type: 'AWS::SQS::Queue',
|
|
127
|
+
Properties: {
|
|
128
|
+
QueueName:
|
|
129
|
+
'internal-error-queue-${self:provider.stage}',
|
|
130
|
+
MessageRetentionPeriod: 300,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
InternalErrorBridgeTopic: {
|
|
134
|
+
Type: 'AWS::SNS::Topic',
|
|
135
|
+
Properties: {
|
|
136
|
+
Subscription: [
|
|
137
|
+
{
|
|
138
|
+
Protocol: 'sqs',
|
|
139
|
+
Endpoint: {
|
|
140
|
+
'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
InternalErrorBridgePolicy: {
|
|
147
|
+
Type: 'AWS::SQS::QueuePolicy',
|
|
148
|
+
Properties: {
|
|
149
|
+
Queues: [{ Ref: 'InternalErrorQueue' }],
|
|
150
|
+
PolicyDocument: {
|
|
151
|
+
Version: '2012-10-17',
|
|
152
|
+
Statement: [
|
|
153
|
+
{
|
|
154
|
+
Sid: 'Allow Dead Letter SNS to publish to SQS',
|
|
155
|
+
Effect: 'Allow',
|
|
156
|
+
Principal: {
|
|
157
|
+
Service: 'sns.amazonaws.com',
|
|
158
|
+
},
|
|
159
|
+
Resource: {
|
|
160
|
+
'Fn::GetAtt': [
|
|
161
|
+
'InternalErrorQueue',
|
|
162
|
+
'Arn',
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
Action: [
|
|
166
|
+
'SQS:SendMessage',
|
|
167
|
+
'SQS:SendMessageBatch',
|
|
168
|
+
],
|
|
169
|
+
Condition: {
|
|
170
|
+
ArnEquals: {
|
|
171
|
+
'aws:SourceArn': {
|
|
172
|
+
Ref: 'InternalErrorBridgeTopic',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
ApiGatewayAlarm5xx: {
|
|
182
|
+
Type: 'AWS::CloudWatch::Alarm',
|
|
183
|
+
Properties: {
|
|
184
|
+
AlarmDescription: 'API Gateway 5xx Errors',
|
|
185
|
+
Namespace: 'AWS/ApiGateway',
|
|
186
|
+
MetricName: '5XXError',
|
|
187
|
+
Statistic: 'Sum',
|
|
188
|
+
Threshold: 0,
|
|
189
|
+
ComparisonOperator: 'GreaterThanThreshold',
|
|
190
|
+
EvaluationPeriods: 1,
|
|
191
|
+
Period: 60,
|
|
192
|
+
AlarmActions: [{ Ref: 'InternalErrorBridgeTopic' }],
|
|
193
|
+
Dimensions: [
|
|
194
|
+
{
|
|
195
|
+
Name: 'ApiName',
|
|
196
|
+
Value: {
|
|
197
|
+
'Fn::Join': [
|
|
198
|
+
'-',
|
|
199
|
+
[
|
|
200
|
+
'${self:provider.stage}',
|
|
201
|
+
'${self:service}',
|
|
202
|
+
],
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Add integration-specific functions and resources
|
|
214
|
+
for (const integration of AppDefinition.integrations) {
|
|
215
|
+
const integrationName = integration.Definition.name;
|
|
216
|
+
|
|
217
|
+
// Add function for the integration
|
|
218
|
+
definition.functions[integrationName] = {
|
|
219
|
+
handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
|
|
220
|
+
events: [
|
|
221
|
+
{
|
|
222
|
+
http: {
|
|
223
|
+
path: `/api/${integrationName}-integration/{proxy+}`,
|
|
224
|
+
method: 'ANY',
|
|
225
|
+
cors: true,
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Add SQS Queue for the integration
|
|
232
|
+
const queueReference = `${
|
|
233
|
+
integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
|
|
234
|
+
}Queue`;
|
|
235
|
+
const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
|
|
236
|
+
definition.resources.Resources[queueReference] = {
|
|
237
|
+
Type: 'AWS::SQS::Queue',
|
|
238
|
+
Properties: {
|
|
239
|
+
QueueName: `\${self:custom.${queueReference}}`,
|
|
240
|
+
MessageRetentionPeriod: 60,
|
|
241
|
+
VisibilityTimeout: 1800, // 30 minutes
|
|
242
|
+
RedrivePolicy: {
|
|
243
|
+
maxReceiveCount: 1,
|
|
244
|
+
deadLetterTargetArn: {
|
|
245
|
+
'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Add Queue Worker for the integration
|
|
252
|
+
const queueWorkerName = `${integrationName}QueueWorker`;
|
|
253
|
+
definition.functions[queueWorkerName] = {
|
|
254
|
+
handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
|
|
255
|
+
reservedConcurrency: 5,
|
|
256
|
+
events: [
|
|
257
|
+
{
|
|
258
|
+
sqs: {
|
|
259
|
+
arn: {
|
|
260
|
+
'Fn::GetAtt': [queueReference, 'Arn'],
|
|
261
|
+
},
|
|
262
|
+
batchSize: 1,
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
timeout: 600,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Add Queue URL for the integration to the ENVironment variables
|
|
270
|
+
definition.provider.environment = {
|
|
271
|
+
...definition.provider.environment,
|
|
272
|
+
[`${integrationName.toUpperCase()}_QUEUE_URL`]: {
|
|
273
|
+
Ref: queueReference,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
definition.custom[queueReference] = queueName;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return definition;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
module.exports = { composeServerlessDefinition };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const slsw = require('serverless-webpack');
|
|
2
|
+
const webpack = require('webpack');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const CopyPlugin = require('copy-webpack-plugin');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
devtool: 'source-map',
|
|
9
|
+
entry: slsw.lib.entries,
|
|
10
|
+
mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
|
|
11
|
+
target: 'node',
|
|
12
|
+
externals: ['mongoose', 'express', 'node-fetch'],
|
|
13
|
+
plugins: [
|
|
14
|
+
// This defines the window global (with value of `undefined`).
|
|
15
|
+
new webpack.ProvidePlugin({
|
|
16
|
+
window: path.resolve(path.join(__dirname, 'src/utils/webpackFakeWindow')),
|
|
17
|
+
}),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/devtools",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0-next.
|
|
4
|
+
"version": "2.0.0-next.10",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@babel/eslint-parser": "^7.18.9",
|
|
7
7
|
"@babel/parser": "^7.25.3",
|
|
8
8
|
"@babel/traverse": "^7.25.3",
|
|
9
|
-
"@friggframework/
|
|
10
|
-
"@
|
|
9
|
+
"@friggframework/test": "2.0.0-next.10",
|
|
10
|
+
"@hapi/boom": "^7.4.11",
|
|
11
|
+
"@inquirer/prompts": "^5.3.8",
|
|
11
12
|
"axios": "^1.7.2",
|
|
13
|
+
"body-parser": "^1.20.2",
|
|
12
14
|
"commander": "^12.1.0",
|
|
15
|
+
"cors": "^2.8.5",
|
|
13
16
|
"dotenv": "^16.4.5",
|
|
14
17
|
"eslint": "^8.22.0",
|
|
15
18
|
"eslint-config-prettier": "^8.5.0",
|
|
@@ -17,12 +20,20 @@
|
|
|
17
20
|
"eslint-plugin-markdown": "^3.0.0",
|
|
18
21
|
"eslint-plugin-no-only-tests": "^3.0.0",
|
|
19
22
|
"eslint-plugin-yaml": "^0.5.0",
|
|
23
|
+
"express": "^4.19.2",
|
|
24
|
+
"express-async-handler": "^1.2.0",
|
|
20
25
|
"fs-extra": "^11.2.0",
|
|
21
|
-
"
|
|
26
|
+
"lodash": "^4.17.21",
|
|
27
|
+
"serverless-http": "^2.7.0"
|
|
22
28
|
},
|
|
23
29
|
"devDependencies": {
|
|
24
|
-
"@friggframework/eslint-config": "
|
|
25
|
-
"@friggframework/prettier-config": "
|
|
30
|
+
"@friggframework/eslint-config": "2.0.0-next.10",
|
|
31
|
+
"@friggframework/prettier-config": "2.0.0-next.10",
|
|
32
|
+
"serverless": "3.39.0",
|
|
33
|
+
"serverless-dotenv-plugin": "^6.0.0",
|
|
34
|
+
"serverless-offline": "^13.8.0",
|
|
35
|
+
"serverless-offline-sqs": "^8.0.0",
|
|
36
|
+
"serverless-webpack": "^5.14.1"
|
|
26
37
|
},
|
|
27
38
|
"scripts": {
|
|
28
39
|
"lint:fix": "prettier --write --loglevel error . && eslint . --fix",
|
|
@@ -46,5 +57,5 @@
|
|
|
46
57
|
"publishConfig": {
|
|
47
58
|
"access": "public"
|
|
48
59
|
},
|
|
49
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "67dc76e94c02c0074f7ff2d7292c4d2203bf3949"
|
|
50
61
|
}
|
package/test/mock-integration.js
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
Auther,
|
|
3
|
+
Credential,
|
|
4
|
+
Entity,
|
|
5
|
+
IntegrationFactory,
|
|
6
|
+
createObjectId,
|
|
7
|
+
} = require('@friggframework/core');
|
|
2
8
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
9
|
+
async function createMockIntegration(
|
|
10
|
+
IntegrationClass,
|
|
11
|
+
userId = null,
|
|
12
|
+
config = { type: IntegrationClass.Definition.name }
|
|
13
|
+
) {
|
|
14
|
+
const integrationFactory = new IntegrationFactory([IntegrationClass]);
|
|
6
15
|
userId = userId || createObjectId();
|
|
7
16
|
|
|
8
17
|
const insertOptions = {
|
|
9
18
|
new: true,
|
|
10
19
|
upsert: true,
|
|
11
20
|
setDefaultsOnInsert: true,
|
|
12
|
-
}
|
|
13
|
-
const user = {user: userId}
|
|
14
|
-
|
|
15
|
-
const credential = await Credential.findOneAndUpdate(
|
|
16
|
-
user,
|
|
17
|
-
{ $set: user },
|
|
18
|
-
insertOptions
|
|
19
|
-
);
|
|
20
|
-
const entity1 = await Entity.findOneAndUpdate(
|
|
21
|
-
user,
|
|
22
|
-
{
|
|
23
|
-
$set: {
|
|
24
|
-
credential: credential.id,
|
|
25
|
-
user: userId,
|
|
26
|
-
name: 'Test user',
|
|
27
|
-
externalId: '1234567890123456',
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
insertOptions
|
|
31
|
-
);
|
|
32
|
-
const entity2 = await Entity.findOneAndUpdate(
|
|
33
|
-
user,
|
|
34
|
-
{
|
|
35
|
-
$set: {
|
|
36
|
-
credential: credential.id,
|
|
37
|
-
user: userId,
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
insertOptions
|
|
41
|
-
);
|
|
21
|
+
};
|
|
22
|
+
const user = { user: userId };
|
|
42
23
|
|
|
43
|
-
const entities = [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
await
|
|
47
|
-
|
|
48
|
-
userId,
|
|
49
|
-
|
|
24
|
+
const entities = [];
|
|
25
|
+
for (const moduleName in IntegrationClass.modules) {
|
|
26
|
+
const ModuleDef = IntegrationClass.Definition.modules[moduleName];
|
|
27
|
+
const module = await Auther.getInstance({
|
|
28
|
+
definition: ModuleDef,
|
|
29
|
+
userId: userId,
|
|
30
|
+
});
|
|
31
|
+
const credential = await module.CredentialModel.findOneAndUpdate(
|
|
32
|
+
user,
|
|
33
|
+
{ $set: user },
|
|
34
|
+
insertOptions
|
|
35
|
+
);
|
|
36
|
+
entities.push(
|
|
37
|
+
(
|
|
38
|
+
await module.EntityModel.findOneAndUpdate(
|
|
39
|
+
user,
|
|
40
|
+
{
|
|
41
|
+
$set: {
|
|
42
|
+
credential,
|
|
43
|
+
user: userId,
|
|
44
|
+
name: `Test ${moduleName}`,
|
|
45
|
+
externalId: `1234567890123456_${moduleName}`,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
insertOptions
|
|
49
|
+
)
|
|
50
|
+
).id
|
|
50
51
|
);
|
|
52
|
+
}
|
|
51
53
|
|
|
52
|
-
integration
|
|
54
|
+
const integration = await integrationFactory.createIntegration(
|
|
55
|
+
entities,
|
|
56
|
+
userId,
|
|
57
|
+
config
|
|
58
|
+
);
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
if (Object.entries(IntegrationClassDef.modules).length <= i) break
|
|
56
|
-
const [moduleName, ModuleDef] = Object.entries(IntegrationClassDef.modules)[i];
|
|
57
|
-
const module = await Auther.getInstance({definition: ModuleDef, userId: userId})
|
|
58
|
-
module.entity = entities[i];
|
|
59
|
-
integration[moduleName] = module;
|
|
60
|
-
}
|
|
60
|
+
integration.id = integration.record._id;
|
|
61
61
|
|
|
62
|
-
return integration
|
|
62
|
+
return integration;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function createMockApiObject(jest, api = {}, mockMethodMap) {
|
|
@@ -67,17 +67,22 @@ function createMockApiObject(jest, api = {}, mockMethodMap) {
|
|
|
67
67
|
// and values which are the mock response (or implementation)
|
|
68
68
|
const clone = (data) => JSON.parse(JSON.stringify(data));
|
|
69
69
|
|
|
70
|
-
for (const [methodName, mockDataOrImplementation] of Object.entries(
|
|
70
|
+
for (const [methodName, mockDataOrImplementation] of Object.entries(
|
|
71
|
+
mockMethodMap
|
|
72
|
+
)) {
|
|
71
73
|
if (mockDataOrImplementation instanceof Function) {
|
|
72
74
|
api[methodName] = jest.fn(mockDataOrImplementation);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
} else if (api[methodName]?.constructor?.name === 'AsyncFunction') {
|
|
76
|
+
api[methodName] = jest
|
|
77
|
+
.fn()
|
|
78
|
+
.mockResolvedValue(clone(mockDataOrImplementation));
|
|
76
79
|
} else {
|
|
77
|
-
api[methodName] = jest
|
|
80
|
+
api[methodName] = jest
|
|
81
|
+
.fn()
|
|
82
|
+
.mockReturnValue(clone(mockDataOrImplementation));
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
return api;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
module.exports = {createMockIntegration, createMockApiObject};
|
|
88
|
+
module.exports = { createMockIntegration, createMockApiObject };
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
const { handleEnvVariables } = require('./environmentVariables');
|
|
2
|
-
const { logInfo } = require('./logger');
|
|
3
|
-
const inquirer = require('inquirer');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const dotenv = require('dotenv');
|
|
6
|
-
const { resolve } = require('node:path');
|
|
7
|
-
const { parse } = require('@babel/parser');
|
|
8
|
-
const traverse = require('@babel/traverse');
|
|
9
|
-
|
|
10
|
-
jest.mock('inquirer');
|
|
11
|
-
jest.mock('fs');
|
|
12
|
-
jest.mock('dotenv');
|
|
13
|
-
jest.mock('./logger');
|
|
14
|
-
jest.mock('@babel/parser');
|
|
15
|
-
jest.mock('@babel/traverse');
|
|
16
|
-
|
|
17
|
-
describe('handleEnvVariables', () => {
|
|
18
|
-
const backendPath = '/mock/backend/path';
|
|
19
|
-
const modulePath = '/mock/module/path';
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
jest.clearAllMocks();
|
|
23
|
-
fs.readFileSync.mockReturnValue(`
|
|
24
|
-
const Definition = {
|
|
25
|
-
env: {
|
|
26
|
-
client_id: process.env.GOOGLE_CALENDAR_CLIENT_ID,
|
|
27
|
-
client_secret: process.env.GOOGLE_CALENDAR_CLIENT_SECRET,
|
|
28
|
-
redirect_uri: \`\${process.env.REDIRECT_URI}/google-calendar\`,
|
|
29
|
-
scope: process.env.GOOGLE_CALENDAR_SCOPE,
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
`);
|
|
33
|
-
parse.mockReturnValue({});
|
|
34
|
-
traverse.default.mockImplementation((ast, visitor) => {
|
|
35
|
-
visitor.ObjectProperty({
|
|
36
|
-
node: {
|
|
37
|
-
key: { name: 'env' },
|
|
38
|
-
value: {
|
|
39
|
-
properties: [
|
|
40
|
-
{ key: { name: 'client_id' }, value: { type: 'MemberExpression', object: { name: 'process' }, property: { name: 'GOOGLE_CALENDAR_CLIENT_ID' } } },
|
|
41
|
-
{ key: { name: 'client_secret' }, value: { type: 'MemberExpression', object: { name: 'process' }, property: { name: 'GOOGLE_CALENDAR_CLIENT_SECRET' } } },
|
|
42
|
-
{ key: { name: 'redirect_uri' }, value: { type: 'MemberExpression', object: { name: 'process' }, property: { name: 'REDIRECT_URI' } } },
|
|
43
|
-
{ key: { name: 'scope' }, value: { type: 'MemberExpression', object: { name: 'process' }, property: { name: 'GOOGLE_CALENDAR_SCOPE' } } },
|
|
44
|
-
]
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
xit('should identify and handle missing environment variables', async () => {
|
|
52
|
-
const localEnvPath = resolve(backendPath, '../.env');
|
|
53
|
-
const localDevConfigPath = resolve(backendPath, '../src/configs/dev.json');
|
|
54
|
-
|
|
55
|
-
fs.existsSync.mockImplementation((path) => path === localEnvPath || path === localDevConfigPath);
|
|
56
|
-
dotenv.parse.mockReturnValue({});
|
|
57
|
-
fs.readFileSync.mockImplementation((path) => {
|
|
58
|
-
if (path === resolve(modulePath, 'index.js')) return 'mock module content';
|
|
59
|
-
if (path === localEnvPath) return '';
|
|
60
|
-
if (path === localDevConfigPath) return '{}';
|
|
61
|
-
return '';
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
inquirer.prompt.mockResolvedValueOnce({ addEnvVars: true })
|
|
65
|
-
.mockResolvedValueOnce({ value: 'client_id_value' })
|
|
66
|
-
.mockResolvedValueOnce({ value: 'client_secret_value' })
|
|
67
|
-
.mockResolvedValueOnce({ value: 'redirect_uri_value' })
|
|
68
|
-
.mockResolvedValueOnce({ value: 'scope_value' });
|
|
69
|
-
|
|
70
|
-
await handleEnvVariables(backendPath, modulePath);
|
|
71
|
-
|
|
72
|
-
expect(logInfo).toHaveBeenCalledWith('Searching for missing environment variables...');
|
|
73
|
-
expect(logInfo).toHaveBeenCalledWith('Missing environment variables: GOOGLE_CALENDAR_CLIENT_ID, GOOGLE_CALENDAR_CLIENT_SECRET, REDIRECT_URI, GOOGLE_CALENDAR_SCOPE');
|
|
74
|
-
expect(inquirer.prompt).toHaveBeenCalledTimes(5);
|
|
75
|
-
expect(fs.appendFileSync).toHaveBeenCalledWith(localEnvPath, '\nGOOGLE_CALENDAR_CLIENT_ID=client_id_value\nGOOGLE_CALENDAR_CLIENT_SECRET=client_secret_value\nREDIRECT_URI=redirect_uri_value\nGOOGLE_CALENDAR_SCOPE=scope_value');
|
|
76
|
-
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
77
|
-
localDevConfigPath,
|
|
78
|
-
JSON.stringify({
|
|
79
|
-
GOOGLE_CALENDAR_CLIENT_ID: 'client_id_value',
|
|
80
|
-
GOOGLE_CALENDAR_CLIENT_SECRET: 'client_secret_value',
|
|
81
|
-
REDIRECT_URI: 'redirect_uri_value',
|
|
82
|
-
GOOGLE_CALENDAR_SCOPE: 'scope_value'
|
|
83
|
-
}, null, 2)
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|