@friggframework/devtools 1.1.6 → 1.1.7-next.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/CHANGELOG.md CHANGED
@@ -1,3 +1,58 @@
1
+ # v1.2.0 (Tue Aug 06 2024)
2
+
3
+ #### 🚀 Enhancement
4
+
5
+ - CLI for Frigg - Install command for now [#322](https://github.com/friggframework/frigg/pull/322) ([@seanspeaks](https://github.com/seanspeaks))
6
+
7
+ #### 🐛 Bug Fix
8
+
9
+ - Add READMEs that will need updating, but for version releasing [#324](https://github.com/friggframework/frigg/pull/324) ([@seanspeaks](https://github.com/seanspeaks))
10
+ - Add READMEs that will need updating, but for version releasing ([@seanspeaks](https://github.com/seanspeaks))
11
+ - small update to integration testing / tooling [#304](https://github.com/friggframework/frigg/pull/304) ([@MichaelRyanWebber](https://github.com/MichaelRyanWebber))
12
+ - Added missing dependencies ([@seanspeaks](https://github.com/seanspeaks))
13
+ - Added a missing dependency ([@seanspeaks](https://github.com/seanspeaks))
14
+ - Updated to handle envs properly, also further refactoring, and better templating. ([@seanspeaks](https://github.com/seanspeaks))
15
+ - WIP with help from Tabnine AI chat. ([@seanspeaks](https://github.com/seanspeaks))
16
+ - Bump version to: v1.1.8 \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks))
17
+ - use the factory methods for creating the mock integration so that everything is set up (mostly events and userActions) ([@MichaelRyanWebber](https://github.com/MichaelRyanWebber))
18
+ - Bump version to: v1.1.5 \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks))
19
+
20
+ #### Authors: 2
21
+
22
+ - [@MichaelRyanWebber](https://github.com/MichaelRyanWebber)
23
+ - Sean Matthews ([@seanspeaks](https://github.com/seanspeaks))
24
+
25
+ ---
26
+
27
+ # v1.1.8 (Thu Jul 18 2024)
28
+
29
+ #### 🐛 Bug Fix
30
+
31
+ - Revert open to support commonjs [#319](https://github.com/friggframework/frigg/pull/319) ([@MichaelRyanWebber](https://github.com/MichaelRyanWebber))
32
+ - Bump version to: v1.1.6 \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks))
33
+
34
+ #### Authors: 2
35
+
36
+ - [@MichaelRyanWebber](https://github.com/MichaelRyanWebber)
37
+ - Sean Matthews ([@seanspeaks](https://github.com/seanspeaks))
38
+
39
+ ---
40
+
41
+ # v1.1.7 (Mon Jul 15 2024)
42
+
43
+ #### 🐛 Bug Fix
44
+
45
+ - getAuthorizationRequirements() async [#318](https://github.com/friggframework/frigg/pull/318) ([@MichaelRyanWebber](https://github.com/MichaelRyanWebber))
46
+ - await getAuthorizeRequirements() ([@MichaelRyanWebber](https://github.com/MichaelRyanWebber))
47
+ - Bump version to: v1.1.6 \[skip ci\] ([@seanspeaks](https://github.com/seanspeaks))
48
+
49
+ #### Authors: 2
50
+
51
+ - [@MichaelRyanWebber](https://github.com/MichaelRyanWebber)
52
+ - Sean Matthews ([@seanspeaks](https://github.com/seanspeaks))
53
+
54
+ ---
55
+
1
56
  # v1.1.6 (Fri Apr 26 2024)
2
57
 
3
58
  #### 🐛 Bug Fix
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # Frigg Framework Devtools
2
+
3
+ This package contains development tools and utilities for the Frigg Framework, an open-source serverless framework designed to simplify the development of integrations at scale.
4
+
5
+ ## Contents
6
+
7
+ The devtools package includes the following main components:
8
+
9
+ 1. Frigg CLI
10
+ 2. Migrations
11
+ 3. Test Utilities
12
+ 4. Local runner and deploy tooling
13
+
14
+ ## Frigg CLI
15
+
16
+ The Frigg CLI is a command-line interface tool that helps developers manage and install API modules in their Frigg projects.
17
+
18
+ ### Key Features
19
+
20
+ - Install API modules
21
+ - Search for available API modules
22
+ - Automatically update project files
23
+ - Handle environment variables
24
+ - Validate package existence and backend paths
25
+
26
+ ### Usage
27
+
28
+ To use the Frigg CLI, run the following command:
29
+ ```sh
30
+ frigg install <api-module-name>
31
+ ```
32
+
33
+ This command will search for the specified API module, install it, and update your project accordingly.
34
+
35
+ ## Migrations
36
+
37
+ (Add information about migrations here if available)
38
+
39
+ ## Test Utilities
40
+
41
+ The test directory contains utilities to assist with testing in the Frigg Framework.
42
+
43
+ ### Key Features
44
+
45
+ - Integration validator (TODO: implementation details to be added)
46
+ - Mock API functionality
47
+
48
+ ### Mock API
49
+
50
+ The `mock-api.js` file provides functionality to mock API responses for testing purposes. It uses `nock` for HTTP request interception and includes features like:
51
+
52
+ - Caching of authentication tokens
53
+ - Recording and replaying of HTTP requests
54
+ - Jest test state management
55
+
56
+ Usage example:
57
+
58
+ ```javascript
59
+ const { mockApi } = require('@friggframework/devtools/test/mock-api');
60
+
61
+ // Use mockApi in your tests to simulate API responses
62
+ ```
63
+ ## Installation
64
+
65
+ To install the devtools package as a dev dependency, run:
66
+
67
+ ```
68
+ npm install --save-dev @friggframework/devtools
69
+ ```
70
+
71
+ ## Contributing
72
+
73
+ Please read [CONTRIBUTING.md](../../CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
74
+
75
+ ## License
76
+
77
+ This project is licensed under the MIT License - see the [LICENSE.md](../../LICENSE.md) file for details
78
+
79
+ ## Support
80
+
81
+ For support, please open an issue in the main Frigg Framework repository or contact the maintainers directly.
@@ -0,0 +1,33 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { logInfo } = require('./logger');
4
+ const INTEGRATIONS_DIR = 'src/integrations';
5
+ const BACKEND_JS = 'backend.js';
6
+
7
+ function updateBackendJsFile(backendPath, apiModuleName) {
8
+ const backendJsPath = path.join(path.dirname(backendPath), BACKEND_JS);
9
+ logInfo(`Updating backend.js: ${backendJsPath}`);
10
+ updateBackendJs(backendJsPath, apiModuleName);
11
+ }
12
+
13
+ function updateBackendJs(backendJsPath, apiModuleName) {
14
+ const backendJsContent = fs.readFileSync(backendJsPath, 'utf-8');
15
+ const importStatement = `const ${apiModuleName}Integration = require('./${INTEGRATIONS_DIR}/${apiModuleName}Integration');\n`;
16
+
17
+ if (!backendJsContent.includes(importStatement)) {
18
+ const updatedContent = backendJsContent.replace(
19
+ /(integrations\s*:\s*\[)([\s\S]*?)(\])/,
20
+ `$1\n ${apiModuleName}Integration,$2$3`
21
+ );
22
+ fs.writeFileSync(backendJsPath, importStatement + updatedContent);
23
+ } else {
24
+ logInfo(
25
+ `Import statement for ${apiModuleName}Integration already exists in backend.js`
26
+ );
27
+ }
28
+ }
29
+
30
+ module.exports = {
31
+ updateBackendJsFile,
32
+ updateBackendJs,
33
+ };
@@ -0,0 +1,26 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const PACKAGE_JSON = 'package.json';
4
+
5
+ function findNearestBackendPackageJson() {
6
+ let currentDir = process.cwd();
7
+ while (currentDir !== path.parse(currentDir).root) {
8
+ const packageJsonPath = path.join(currentDir, 'backend', PACKAGE_JSON);
9
+ if (fs.existsSync(packageJsonPath)) {
10
+ return packageJsonPath;
11
+ }
12
+ currentDir = path.dirname(currentDir);
13
+ }
14
+ return null;
15
+ }
16
+
17
+ function validateBackendPath(backendPath) {
18
+ if (!backendPath) {
19
+ throw new Error('Could not find a backend package.json file.');
20
+ }
21
+ }
22
+
23
+ module.exports = {
24
+ findNearestBackendPackageJson,
25
+ validateBackendPath,
26
+ };
@@ -0,0 +1,16 @@
1
+ const { execSync } = require('child_process');
2
+ const path = require('path');
3
+
4
+ function commitChanges(backendPath, apiModuleName) {
5
+ const apiModulePath = path.join(path.dirname(backendPath), 'src', 'integrations', `${apiModuleName}Integration.js`);
6
+ try {
7
+ execSync(`git add ${apiModulePath}`);
8
+ execSync(`git commit -m "Add ${apiModuleName}Integration to ${apiModuleName}Integration.js"`);
9
+ } catch (error) {
10
+ throw new Error('Failed to commit changes:', error);
11
+ }
12
+ }
13
+
14
+ module.exports = {
15
+ commitChanges,
16
+ };
@@ -0,0 +1,134 @@
1
+ const fs = require('fs');
2
+ const dotenv = require('dotenv');
3
+ const { readFileSync, writeFileSync, existsSync } = require('fs');
4
+ const { logInfo } = require('./logger');
5
+ const { resolve } = require('node:path');
6
+ const inquirer = require('inquirer');
7
+
8
+ const { parse } = require('@babel/parser');
9
+ const traverse = require('@babel/traverse').default;
10
+
11
+ const extractRawEnvVariables = (modulePath) => {
12
+ const filePath = resolve(modulePath, 'definition.js');
13
+
14
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
15
+ const ast = parse(fileContent, {
16
+ sourceType: 'module',
17
+ plugins: ['jsx', 'typescript'], // Add more plugins if needed
18
+ });
19
+
20
+ const envVariables = {};
21
+
22
+ traverse(ast, {
23
+ ObjectProperty(path) {
24
+ if (path.node.key.name === 'env') {
25
+ path.node.value.properties.forEach((prop) => {
26
+ const key = prop.key.name;
27
+ if (prop.value.type === 'MemberExpression') {
28
+ const property = prop.value.property.name;
29
+ envVariables[key] = `${property}`;
30
+ } else if (prop.value.type === 'TemplateLiteral') {
31
+ // Handle template literals
32
+ const expressions = prop.value.expressions.map((exp) =>
33
+ exp.type === 'MemberExpression'
34
+ ? `${exp.property.name}`
35
+ : exp.name
36
+ );
37
+ envVariables[key] = expressions.join('');
38
+ }
39
+ });
40
+ }
41
+ },
42
+ });
43
+
44
+ return envVariables;
45
+ };
46
+ const handleEnvVariables = async (backendPath, modulePath) => {
47
+ logInfo('Searching for missing environment variables...');
48
+ const Definition = { env: extractRawEnvVariables(modulePath) };
49
+ if (Definition && Definition.env) {
50
+ console.log('Here is Definition.env:', Definition.env);
51
+ const envVars = Object.values(Definition.env);
52
+
53
+ console.log(
54
+ 'Found the following environment variables in the API module:',
55
+ envVars
56
+ );
57
+
58
+ const localEnvPath = resolve(backendPath, '../.env');
59
+ const localDevConfigPath = resolve(
60
+ backendPath,
61
+ '../src/configs/dev.json'
62
+ );
63
+
64
+ // Load local .env variables
65
+ let localEnvVars = {};
66
+ if (existsSync(localEnvPath)) {
67
+ localEnvVars = dotenv.parse(readFileSync(localEnvPath, 'utf8'));
68
+ }
69
+
70
+ // Load local dev.json variables
71
+ let localDevConfig = {};
72
+ if (existsSync(localDevConfigPath)) {
73
+ localDevConfig = JSON.parse(
74
+ readFileSync(localDevConfigPath, 'utf8')
75
+ );
76
+ }
77
+
78
+ const missingEnvVars = envVars.filter(
79
+ (envVar) => !localEnvVars[envVar] && !localDevConfig[envVar]
80
+ );
81
+
82
+ logInfo(`Missing environment variables: ${missingEnvVars.join(', ')}`);
83
+
84
+ if (missingEnvVars.length > 0) {
85
+ const { addEnvVars } = await inquirer.prompt([
86
+ {
87
+ type: 'confirm',
88
+ name: 'addEnvVars',
89
+ message: `The following environment variables are required: ${missingEnvVars.join(
90
+ ', '
91
+ )}. Do you want to add them now?`,
92
+ },
93
+ ]);
94
+
95
+ if (addEnvVars) {
96
+ const envValues = {};
97
+ for (const envVar of missingEnvVars) {
98
+ const { value } = await inquirer.prompt([
99
+ {
100
+ type: 'input',
101
+ name: 'value',
102
+ message: `Enter value for ${envVar}:`,
103
+ },
104
+ ]);
105
+ envValues[envVar] = value;
106
+ }
107
+
108
+ // Add the envValues to the local .env file if it exists
109
+ if (existsSync(localEnvPath)) {
110
+ const envContent = Object.entries(envValues)
111
+ .map(([key, value]) => `${key}=${value}`)
112
+ .join('\n');
113
+ fs.appendFileSync(localEnvPath, `\n${envContent}`);
114
+ }
115
+
116
+ // Add the envValues to the local dev.json file if it exists
117
+ if (existsSync(localDevConfigPath)) {
118
+ const updatedDevConfig = {
119
+ ...localDevConfig,
120
+ ...envValues,
121
+ };
122
+ writeFileSync(
123
+ localDevConfigPath,
124
+ JSON.stringify(updatedDevConfig, null, 2)
125
+ );
126
+ }
127
+ } else {
128
+ logInfo("Edit whenever you're able, safe travels friend!");
129
+ }
130
+ }
131
+ }
132
+ };
133
+
134
+ module.exports = { handleEnvVariables };
@@ -0,0 +1,86 @@
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
+ });
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const { installCommand } = require('./installCommand');
5
+
6
+ const program = new Command();
7
+ program
8
+ .command('install [apiModuleName]')
9
+ .description('Install an API module')
10
+ .action(installCommand);
11
+
12
+ program.parse(process.argv);
13
+
14
+ module.exports = { installCommand };
@@ -0,0 +1,109 @@
1
+ const { Command } = require('commander');
2
+ const { installCommand } = require('./index');
3
+ const { validatePackageExists } = require('./validatePackage');
4
+ const { findNearestBackendPackageJson, validateBackendPath } = require('./backendPath');
5
+ const { installPackage } = require('./installPackage');
6
+ const { createIntegrationFile } = require('./integrationFile');
7
+ const { updateBackendJsFile } = require('./backendJs');
8
+ const { commitChanges } = require('./commitChanges');
9
+ const { logInfo, logError } = require('./logger');
10
+
11
+ describe('CLI Command Tests', () => {
12
+ it('should successfully install an API module when all steps complete without errors', async () => {
13
+ const mockApiModuleName = 'testModule';
14
+ const mockPackageName = `@friggframework/api-module-${mockApiModuleName}`;
15
+ const mockBackendPath = '/mock/backend/path';
16
+
17
+ jest.mock('./validatePackage', () => ({
18
+ validatePackageExists: jest.fn().mockResolvedValue(true),
19
+ }));
20
+ jest.mock('./backendPath', () => ({
21
+ findNearestBackendPackageJson: jest.fn().mockReturnValue(mockBackendPath),
22
+ validateBackendPath: jest.fn().mockReturnValue(true),
23
+ }));
24
+ jest.mock('./installPackage', () => ({
25
+ installPackage: jest.fn().mockReturnValue(true),
26
+ }));
27
+ jest.mock('./integrationFile', () => ({
28
+ createIntegrationFile: jest.fn().mockReturnValue(true),
29
+ }));
30
+ jest.mock('./backendJs', () => ({
31
+ updateBackendJsFile: jest.fn().mockReturnValue(true),
32
+ }));
33
+ jest.mock('./commitChanges', () => ({
34
+ commitChanges: jest.fn().mockReturnValue(true),
35
+ }));
36
+ jest.mock('./logger', () => ({
37
+ logInfo: jest.fn(),
38
+ logError: jest.fn(),
39
+ }));
40
+
41
+ const program = new Command();
42
+ program
43
+ .command('install <apiModuleName>')
44
+ .description('Install an API module')
45
+ .action(installCommand);
46
+
47
+ await program.parseAsync(['node', 'install', mockApiModuleName]);
48
+
49
+ expect(validatePackageExists).toHaveBeenCalledWith(mockPackageName);
50
+ expect(findNearestBackendPackageJson).toHaveBeenCalled();
51
+ expect(validateBackendPath).toHaveBeenCalledWith(mockBackendPath);
52
+ expect(installPackage).toHaveBeenCalledWith(mockBackendPath, mockPackageName);
53
+ expect(createIntegrationFile).toHaveBeenCalledWith(mockBackendPath, mockApiModuleName);
54
+ expect(updateBackendJsFile).toHaveBeenCalledWith(mockBackendPath, mockApiModuleName);
55
+ expect(commitChanges).toHaveBeenCalledWith(mockBackendPath, mockApiModuleName);
56
+ expect(logInfo).toHaveBeenCalledWith(`Successfully installed ${mockPackageName} and updated the project.`);
57
+ });
58
+
59
+ it('should log an error and exit with code 1 if the package does not exist', async () => {
60
+ const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
61
+ const mockLogError = jest.spyOn(require('./logger'), 'logError').mockImplementation(() => {});
62
+ const mockValidatePackageExists = jest.spyOn(require('./validatePackage'), 'validatePackageExists').mockImplementation(() => {
63
+ throw new Error('Package not found');
64
+ });
65
+
66
+ const program = new Command();
67
+ program
68
+ .command('install <apiModuleName>')
69
+ .description('Install an API module')
70
+ .action(installCommand);
71
+
72
+ await program.parseAsync(['node', 'install', 'nonexistent-package']);
73
+
74
+ expect(mockValidatePackageExists).toHaveBeenCalledWith('@friggframework/api-module-nonexistent-package');
75
+ expect(mockLogError).toHaveBeenCalledWith('An error occurred:', expect.any(Error));
76
+ expect(mockExit).toHaveBeenCalledWith(1);
77
+
78
+ mockExit.mockRestore();
79
+ mockLogError.mockRestore();
80
+ mockValidatePackageExists.mockRestore();
81
+ });
82
+
83
+ it('should log an error and exit with code 1 if the backend path is invalid', async () => {
84
+ const mockLogError = jest.spyOn(require('./logger'), 'logError').mockImplementation(() => {});
85
+ const mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
86
+ const mockValidatePackageExists = jest.spyOn(require('./validatePackage'), 'validatePackageExists').mockResolvedValue(true);
87
+ const mockFindNearestBackendPackageJson = jest.spyOn(require('./backendPath'), 'findNearestBackendPackageJson').mockReturnValue('/invalid/path');
88
+ const mockValidateBackendPath = jest.spyOn(require('./backendPath'), 'validateBackendPath').mockImplementation(() => {
89
+ throw new Error('Invalid backend path');
90
+ });
91
+
92
+ const program = new Command();
93
+ program
94
+ .command('install <apiModuleName>')
95
+ .description('Install an API module')
96
+ .action(installCommand);
97
+
98
+ await program.parseAsync(['node', 'install', 'test-module']);
99
+
100
+ expect(mockLogError).toHaveBeenCalledWith('An error occurred:', expect.any(Error));
101
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
102
+
103
+ mockLogError.mockRestore();
104
+ mockProcessExit.mockRestore();
105
+ mockValidatePackageExists.mockRestore();
106
+ mockFindNearestBackendPackageJson.mockRestore();
107
+ mockValidateBackendPath.mockRestore();
108
+ });
109
+ });
@@ -0,0 +1,57 @@
1
+ const { installPackage } = require('./installPackage');
2
+ const { createIntegrationFile } = require('./integrationFile');
3
+ const { resolve } = require('node:path');
4
+ const { updateBackendJsFile } = require('./backendJs');
5
+ const { logInfo, logError } = require('./logger');
6
+ const { commitChanges } = require('./commitChanges');
7
+ const {
8
+ findNearestBackendPackageJson,
9
+ validateBackendPath,
10
+ } = require('./backendPath');
11
+ const { handleEnvVariables } = require('./environmentVariables');
12
+ const {
13
+ validatePackageExists,
14
+ searchAndSelectPackage,
15
+ } = require('./validatePackage');
16
+
17
+ const installCommand = async (apiModuleName) => {
18
+ try {
19
+ const packageNames = await searchAndSelectPackage(apiModuleName);
20
+ if (!packageNames || packageNames.length === 0) return;
21
+
22
+ const backendPath = findNearestBackendPackageJson();
23
+ validateBackendPath(backendPath);
24
+
25
+ for (const packageName of packageNames) {
26
+ await validatePackageExists(packageName);
27
+ installPackage(backendPath, packageName);
28
+
29
+ const modulePath = resolve(
30
+ backendPath,
31
+ `../../node_modules/${packageName}`
32
+ );
33
+ const {
34
+ Config: { label },
35
+ Api: ApiClass,
36
+ } = require(modulePath);
37
+
38
+ const sanitizedLabel = label.replace(
39
+ /[<>:"/\\|?*\x00-\x1F\s]/g,
40
+ ''
41
+ ); // Remove invalid characters and spaces console.log('Installing integration for:', sanitizedLabel);
42
+ createIntegrationFile(backendPath, sanitizedLabel, ApiClass);
43
+ updateBackendJsFile(backendPath, sanitizedLabel);
44
+ commitChanges(backendPath, sanitizedLabel);
45
+ logInfo(
46
+ `Successfully installed ${packageName} and updated the project.`
47
+ );
48
+
49
+ await handleEnvVariables(backendPath, modulePath);
50
+ }
51
+ } catch (error) {
52
+ logError('An error occurred:', error);
53
+ process.exit(1);
54
+ }
55
+ };
56
+
57
+ module.exports = { installCommand };
@@ -0,0 +1,13 @@
1
+ const { execSync } = require('child_process');
2
+ const path = require('path');
3
+
4
+ function installPackage(backendPath, packageName) {
5
+ execSync(`npm install ${packageName}`, {
6
+ cwd: path.dirname(backendPath),
7
+ stdio: 'inherit',
8
+ });
9
+ }
10
+
11
+ module.exports = {
12
+ installPackage,
13
+ };
@@ -0,0 +1,30 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { logInfo } = require('./logger');
4
+ const { getIntegrationTemplate } = require('./template');
5
+ const INTEGRATIONS_DIR = 'src/integrations';
6
+
7
+ function createIntegrationFile(backendPath, apiModuleName, ApiClass) {
8
+ const integrationDir = path.join(
9
+ path.dirname(backendPath),
10
+ INTEGRATIONS_DIR
11
+ );
12
+ logInfo(`Ensuring directory exists: ${integrationDir}`);
13
+ fs.ensureDirSync(integrationDir);
14
+
15
+ const integrationFilePath = path.join(
16
+ integrationDir,
17
+ `${apiModuleName}Integration.js`
18
+ );
19
+ logInfo(`Writing integration file: ${integrationFilePath}`);
20
+ const integrationTemplate = getIntegrationTemplate(
21
+ apiModuleName,
22
+ backendPath,
23
+ ApiClass
24
+ );
25
+ fs.writeFileSync(integrationFilePath, integrationTemplate);
26
+ }
27
+
28
+ module.exports = {
29
+ createIntegrationFile,
30
+ };
@@ -0,0 +1,12 @@
1
+ function logInfo(message) {
2
+ console.log(message);
3
+ }
4
+
5
+ function logError(message, error) {
6
+ console.error(message, error);
7
+ }
8
+
9
+ module.exports = {
10
+ logInfo,
11
+ logError,
12
+ };
@@ -0,0 +1,90 @@
1
+ const path = require('path');
2
+
3
+ function getIntegrationTemplate(apiModuleName, backendPath, ApiClass) {
4
+ // Find the sample data method
5
+ const apiMethods = Object.getOwnPropertyNames(ApiClass.prototype);
6
+ const sampleDataMethod =
7
+ apiMethods.find(
8
+ (method) =>
9
+ method.toLowerCase().includes('search') ||
10
+ method.toLowerCase().includes('list') ||
11
+ method.toLowerCase().includes('get')
12
+ ) || 'searchDeals';
13
+
14
+ return `const { get, IntegrationBase, Options } = require('@friggframework/core');
15
+ const { Definition: ${apiModuleName}Module, Config: defaultConfig } = require('@friggframework/api-module-${apiModuleName.toLowerCase()}');
16
+
17
+ class ${apiModuleName}Integration extends IntegrationBase {
18
+ static Config = {
19
+ name: defaultConfig.name || '${apiModuleName.toLowerCase()}',
20
+ version: '1.0.0',
21
+ supportedVersions: ['1.0.0'],
22
+ events: ['SEARCH_DEALS'],
23
+ };
24
+
25
+ static Options =
26
+ new Options({
27
+ module: ${apiModuleName}Module,
28
+ integrations: [${apiModuleName}Module],
29
+ display: {
30
+ name: defaultConfig.displayName || '${apiModuleName}',
31
+ description: defaultConfig.description || 'Sales & CRM, Marketing',
32
+ category: defaultConfig.category || 'Sales & CRM, Marketing',
33
+ detailsUrl: defaultConfig.detailsUrl || 'https://www.${apiModuleName.toLowerCase()}.com',
34
+ icon: defaultConfig.icon || 'https://friggframework.org/assets/img/${apiModuleName.toLowerCase()}.jpeg',
35
+ },
36
+ hasUserConfig: true,
37
+ });
38
+
39
+ static modules = {
40
+ ${apiModuleName.toLowerCase()}: ${apiModuleName}Module,
41
+ }
42
+
43
+ /**
44
+ * HANDLE EVENTS
45
+ */
46
+ async receiveNotification(notifier, event, object = null) {
47
+ if (event === 'SEARCH_DEALS') {
48
+ return this.target.api.searchDeals(object);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * ALL CUSTOM/OPTIONAL METHODS FOR AN INTEGRATION MANAGER
54
+ */
55
+ async getSampleData() {
56
+ const res = await this.target.api.${sampleDataMethod}();
57
+ return { data: res };
58
+ }
59
+
60
+ /**
61
+ * ALL REQUIRED METHODS FOR AN INTEGRATION MANAGER
62
+ */
63
+ async onCreate(params) {
64
+ // Validate that we have all of the data we need
65
+ // Set integration status as makes sense. Default ENABLED
66
+ // TODO turn this into a validateConfig method/function
67
+ this.record.status = 'ENABLED';
68
+ await this.record.save();
69
+ return this.record;
70
+ }
71
+
72
+ async onUpdate(params) {
73
+ const newConfig = get(params, 'config');
74
+ const oldConfig = this.record.config;
75
+ // Just save whatever
76
+ this.record.markModified('config');
77
+ await this.record.save();
78
+ return this.validateConfig();
79
+ }
80
+
81
+ async getConfigOptions() {
82
+ const options = {}
83
+ return options;
84
+ }
85
+ }
86
+
87
+ module.exports = ${apiModuleName}Integration;`;
88
+ }
89
+
90
+ module.exports = { getIntegrationTemplate };
@@ -0,0 +1,79 @@
1
+ const { execSync } = require('child_process');
2
+ const axios = require('axios');
3
+ const { logError } = require('./logger');
4
+ const inquirer = require('inquirer');
5
+
6
+ async function searchPackages(apiModuleName) {
7
+ const searchCommand = `npm search @friggframework/api-module-${apiModuleName} --json`;
8
+ const result = execSync(searchCommand, { encoding: 'utf8' });
9
+ return JSON.parse(result);
10
+ }
11
+
12
+ async function checkPackageExists(packageName) {
13
+ try {
14
+ const response = await axios.get(
15
+ `https://registry.npmjs.org/${packageName}`
16
+ );
17
+ return response.status === 200;
18
+ } catch (error) {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ async function validatePackageExists(packageName) {
24
+ const packageExists = await checkPackageExists(packageName);
25
+ if (!packageExists) {
26
+ throw new Error(`Package ${packageName} does not exist on npm.`);
27
+ }
28
+ }
29
+
30
+ const searchAndSelectPackage = async (apiModuleName) => {
31
+ const searchResults = await searchPackages(apiModuleName || '');
32
+
33
+ if (searchResults.length === 0) {
34
+ logError(`No packages found matching ${apiModuleName}`);
35
+ process.exit(1);
36
+ }
37
+
38
+ const filteredResults = searchResults.filter((pkg) => {
39
+ const version = pkg.version
40
+ ? pkg.version.split('.').map(Number)
41
+ : [];
42
+ return version[0] >= 1;
43
+ });
44
+
45
+ if (filteredResults.length === 0) {
46
+ const earlierVersions = searchResults
47
+ .map((pkg) => `${pkg.name} (${pkg.version})`)
48
+ .join(', ');
49
+ logError(
50
+ `No packages found with version 1.0.0 or above for ${apiModuleName}. Found earlier versions: ${earlierVersions}`
51
+ );
52
+ process.exit(1);
53
+ }
54
+
55
+ const choices = filteredResults.map((pkg) => {
56
+ return {
57
+ name: `${pkg.name} (${pkg.version})`,
58
+ checked: filteredResults.length === 1, // Automatically select if only one result
59
+ };
60
+ });
61
+
62
+ const { selectedPackages } = await inquirer.prompt([
63
+ {
64
+ type: 'checkbox',
65
+ name: 'selectedPackages',
66
+ message: 'Select the packages to install:',
67
+ choices,
68
+ },
69
+ ]);
70
+
71
+ return selectedPackages.map(choice => choice.split(' ')[0]);
72
+ };
73
+
74
+ module.exports = {
75
+ validatePackageExists,
76
+ checkPackageExists,
77
+ searchPackages,
78
+ searchAndSelectPackage,
79
+ };
package/package.json CHANGED
@@ -1,26 +1,36 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "1.1.6",
4
+ "version": "1.1.7-next.1",
5
5
  "dependencies": {
6
6
  "@babel/eslint-parser": "^7.18.9",
7
- "@friggframework/core": "^1.1.6",
8
- "@friggframework/test": "^1.1.6",
7
+ "@babel/parser": "^7.25.3",
8
+ "@babel/traverse": "^7.25.3",
9
+ "@friggframework/core": "1.1.7-next.1",
10
+ "@friggframework/test": "1.1.7-next.1",
11
+ "axios": "^1.7.2",
12
+ "commander": "^12.1.0",
13
+ "dotenv": "^16.4.5",
9
14
  "eslint": "^8.22.0",
10
15
  "eslint-config-prettier": "^8.5.0",
11
16
  "eslint-plugin-json": "^3.1.0",
12
17
  "eslint-plugin-markdown": "^3.0.0",
13
18
  "eslint-plugin-no-only-tests": "^3.0.0",
14
- "eslint-plugin-yaml": "^0.5.0"
19
+ "eslint-plugin-yaml": "^0.5.0",
20
+ "fs-extra": "^11.2.0",
21
+ "inquirer": "^10.1.6"
15
22
  },
16
23
  "devDependencies": {
17
- "@friggframework/eslint-config": "^1.1.6",
18
- "@friggframework/prettier-config": "^1.1.6"
24
+ "@friggframework/eslint-config": "1.1.7-next.1",
25
+ "@friggframework/prettier-config": "1.1.7-next.1"
19
26
  },
20
27
  "scripts": {
21
28
  "lint:fix": "prettier --write --loglevel error . && eslint . --fix",
22
29
  "test": "jest --passWithNoTests # TODO"
23
30
  },
31
+ "bin": {
32
+ "frigg": "./frigg-cli/index.js"
33
+ },
24
34
  "author": "",
25
35
  "license": "MIT",
26
36
  "main": "index.js",
@@ -36,5 +46,5 @@
36
46
  "publishConfig": {
37
47
  "access": "public"
38
48
  },
39
- "gitHead": "49897aff107e2fb5b3cfe78a09106766bd34ca28"
49
+ "gitHead": "064b36b36e6600ea126bde63ea05890ee87795a3"
40
50
  }
@@ -52,7 +52,7 @@ function testAutherDefinition(definition, mocks) {
52
52
  authCallbackParams = mocks.authorizeResponse || mocks.authorizeParams;
53
53
  describe('getAuthorizationRequirements() test', () => {
54
54
  it('should return auth requirements', async () => {
55
- requirements = module.getAuthorizationRequirements();
55
+ requirements = await module.getAuthorizationRequirements();
56
56
  expect(requirements).toBeDefined();
57
57
  expect(requirements.type).toEqual(ModuleConstants.authType.oauth2);
58
58
  expect(requirements.url).toBeDefined();
@@ -1,10 +1,9 @@
1
- const { Auther, Credential, Entity, IntegrationModel, mongoose } = require('@friggframework/core');
1
+ const { Auther, Credential, Entity, IntegrationFactory, createObjectId } = require('@friggframework/core');
2
2
 
3
3
 
4
4
  async function createMockIntegration(IntegrationClassDef, userId = null, config = {},) {
5
- const integration = new IntegrationClassDef();
6
- userId = userId || new mongoose.Types.ObjectId();
7
- integration.delegateTypes.push(...IntegrationClassDef.Config.events)
5
+ const integrationFactory = new IntegrationFactory([IntegrationClassDef]);
6
+ userId = userId || createObjectId();
8
7
 
9
8
  const insertOptions = {
10
9
  new: true,
@@ -42,11 +41,13 @@ async function createMockIntegration(IntegrationClassDef, userId = null, config
42
41
  );
43
42
 
44
43
  const entities = [entity1, entity2]
45
- integration.record = await IntegrationModel.create({
46
- entities,
47
- user: userId,
48
- config: {type: IntegrationClassDef.Config.name, ...config}
49
- })
44
+
45
+ const integration =
46
+ await integrationFactory.createIntegration(
47
+ entities,
48
+ userId,
49
+ config,
50
+ );
50
51
 
51
52
  integration.id = integration.record._id
52
53