@friggframework/devtools 2.0.0--canary.548.c8ae0ca.0 → 2.0.0--canary.545.c40eca4.0
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/README.md +1 -1
- package/frigg-cli/__tests__/application/use-cases/AddApiModuleToIntegrationUseCase.test.js +326 -0
- package/frigg-cli/__tests__/application/use-cases/CreateApiModuleUseCase.test.js +337 -0
- package/frigg-cli/__tests__/domain/entities/ApiModule.test.js +373 -0
- package/frigg-cli/__tests__/domain/entities/AppDefinition.test.js +313 -0
- package/frigg-cli/__tests__/domain/services/IntegrationValidator.test.js +269 -0
- package/frigg-cli/__tests__/domain/value-objects/IntegrationName.test.js +82 -0
- package/frigg-cli/__tests__/infrastructure/adapters/IntegrationJsUpdater.test.js +408 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemApiModuleRepository.test.js +583 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemAppDefinitionRepository.test.js +314 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemIntegrationRepository.test.js +383 -0
- package/frigg-cli/__tests__/unit/commands/build.test.js +1 -1
- package/frigg-cli/__tests__/unit/commands/doctor.test.js +0 -2
- package/frigg-cli/__tests__/unit/commands/init.test.js +406 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +23 -19
- package/frigg-cli/__tests__/unit/commands/provider-dispatch.test.js +383 -0
- package/frigg-cli/__tests__/unit/commands/repair.test.js +275 -0
- package/frigg-cli/__tests__/unit/dependencies.test.js +2 -2
- package/frigg-cli/__tests__/unit/start-command/application/RunPreflightChecksUseCase.test.js +411 -0
- package/frigg-cli/__tests__/unit/start-command/infrastructure/DatabaseAdapter.test.js +405 -0
- package/frigg-cli/__tests__/unit/start-command/infrastructure/DockerAdapter.test.js +496 -0
- package/frigg-cli/__tests__/unit/start-command/presentation/InteractivePromptAdapter.test.js +474 -0
- package/frigg-cli/__tests__/unit/utils/output.test.js +196 -0
- package/frigg-cli/application/use-cases/AddApiModuleToIntegrationUseCase.js +93 -0
- package/frigg-cli/application/use-cases/CreateApiModuleUseCase.js +93 -0
- package/frigg-cli/application/use-cases/CreateIntegrationUseCase.js +103 -0
- package/frigg-cli/build-command/index.js +123 -11
- package/frigg-cli/container.js +172 -0
- package/frigg-cli/deploy-command/index.js +83 -1
- package/frigg-cli/docs/OUTPUT_MIGRATION_GUIDE.md +286 -0
- package/frigg-cli/doctor-command/index.js +37 -16
- package/frigg-cli/domain/entities/ApiModule.js +272 -0
- package/frigg-cli/domain/entities/AppDefinition.js +227 -0
- package/frigg-cli/domain/entities/Integration.js +198 -0
- package/frigg-cli/domain/exceptions/DomainException.js +24 -0
- package/frigg-cli/domain/ports/IApiModuleRepository.js +53 -0
- package/frigg-cli/domain/ports/IAppDefinitionRepository.js +43 -0
- package/frigg-cli/domain/ports/IIntegrationRepository.js +61 -0
- package/frigg-cli/domain/services/IntegrationValidator.js +185 -0
- package/frigg-cli/domain/value-objects/IntegrationId.js +42 -0
- package/frigg-cli/domain/value-objects/IntegrationName.js +60 -0
- package/frigg-cli/domain/value-objects/SemanticVersion.js +70 -0
- package/frigg-cli/generate-iam-command.js +21 -1
- package/frigg-cli/index.js +21 -6
- package/frigg-cli/index.test.js +7 -2
- package/frigg-cli/infrastructure/UnitOfWork.js +46 -0
- package/frigg-cli/infrastructure/adapters/BackendJsUpdater.js +197 -0
- package/frigg-cli/infrastructure/adapters/FileSystemAdapter.js +224 -0
- package/frigg-cli/infrastructure/adapters/IntegrationJsUpdater.js +249 -0
- package/frigg-cli/infrastructure/adapters/SchemaValidator.js +92 -0
- package/frigg-cli/infrastructure/repositories/FileSystemApiModuleRepository.js +373 -0
- package/frigg-cli/infrastructure/repositories/FileSystemAppDefinitionRepository.js +116 -0
- package/frigg-cli/infrastructure/repositories/FileSystemIntegrationRepository.js +277 -0
- package/frigg-cli/init-command/backend-first-handler.js +124 -42
- package/frigg-cli/init-command/index.js +2 -1
- package/frigg-cli/init-command/template-handler.js +13 -3
- package/frigg-cli/install-command/backend-js.js +3 -3
- package/frigg-cli/install-command/environment-variables.js +16 -19
- package/frigg-cli/install-command/environment-variables.test.js +12 -13
- package/frigg-cli/install-command/index.js +14 -9
- package/frigg-cli/install-command/integration-file.js +3 -3
- package/frigg-cli/install-command/validate-package.js +5 -9
- package/frigg-cli/jest.config.js +4 -1
- package/frigg-cli/package-lock.json +16226 -0
- package/frigg-cli/repair-command/index.js +121 -128
- package/frigg-cli/start-command/application/RunPreflightChecksUseCase.js +376 -0
- package/frigg-cli/start-command/index.js +324 -2
- package/frigg-cli/start-command/infrastructure/DatabaseAdapter.js +591 -0
- package/frigg-cli/start-command/infrastructure/DockerAdapter.js +306 -0
- package/frigg-cli/start-command/presentation/InteractivePromptAdapter.js +329 -0
- package/frigg-cli/templates/backend/.env.example +62 -0
- package/frigg-cli/templates/backend/.eslintrc.json +12 -0
- package/frigg-cli/templates/backend/.prettierrc +6 -0
- package/frigg-cli/templates/backend/docker-compose.yml +22 -0
- package/frigg-cli/templates/backend/index.js +96 -0
- package/frigg-cli/templates/backend/infrastructure.js +12 -0
- package/frigg-cli/templates/backend/jest.config.js +17 -0
- package/frigg-cli/templates/backend/package.json +50 -0
- package/frigg-cli/templates/backend/src/api-modules/.gitkeep +10 -0
- package/frigg-cli/templates/backend/src/base/.gitkeep +7 -0
- package/frigg-cli/templates/backend/src/integrations/.gitkeep +10 -0
- package/frigg-cli/templates/backend/src/integrations/ExampleIntegration.js +65 -0
- package/frigg-cli/templates/backend/src/utils/.gitkeep +7 -0
- package/frigg-cli/templates/backend/test/setup.js +30 -0
- package/frigg-cli/templates/backend/ui-extensions/.gitkeep +0 -0
- package/frigg-cli/templates/backend/ui-extensions/README.md +77 -0
- package/frigg-cli/ui-command/index.js +58 -36
- package/frigg-cli/utils/__tests__/provider-helper.test.js +55 -0
- package/frigg-cli/utils/__tests__/repo-detection.test.js +436 -0
- package/frigg-cli/utils/output.js +382 -0
- package/frigg-cli/utils/provider-helper.js +75 -0
- package/frigg-cli/utils/repo-detection.js +85 -37
- package/frigg-cli/validate-command/__tests__/adapters/validate-command.test.js +205 -0
- package/frigg-cli/validate-command/__tests__/application/validate-app-use-case.test.js +104 -0
- package/frigg-cli/validate-command/__tests__/domain/fix-suggestion.test.js +153 -0
- package/frigg-cli/validate-command/__tests__/domain/validation-error.test.js +162 -0
- package/frigg-cli/validate-command/__tests__/domain/validation-result.test.js +152 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/api-module-validator.test.js +332 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/app-definition-validator.test.js +191 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/integration-class-validator.test.js +146 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/template-validation.test.js +155 -0
- package/frigg-cli/validate-command/adapters/cli/validate-command.js +199 -0
- package/frigg-cli/validate-command/application/use-cases/validate-app-use-case.js +35 -0
- package/frigg-cli/validate-command/domain/entities/validation-result.js +74 -0
- package/frigg-cli/validate-command/domain/value-objects/fix-suggestion.js +74 -0
- package/frigg-cli/validate-command/domain/value-objects/validation-error.js +68 -0
- package/frigg-cli/validate-command/infrastructure/validators/api-module-validator.js +181 -0
- package/frigg-cli/validate-command/infrastructure/validators/app-definition-validator.js +128 -0
- package/frigg-cli/validate-command/infrastructure/validators/integration-class-validator.js +113 -0
- package/infrastructure/create-frigg-infrastructure.js +93 -0
- package/infrastructure/docs/iam-policy-templates.md +1 -1
- package/infrastructure/domains/admin-scripts/admin-script-builder.js +200 -0
- package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +499 -0
- package/infrastructure/domains/admin-scripts/index.js +5 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +2 -4
- package/infrastructure/domains/networking/vpc-resolver.test.js +1 -1
- package/infrastructure/domains/shared/resource-discovery.js +5 -5
- package/infrastructure/domains/shared/types/app-definition.js +21 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +1 -1
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +10 -1
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +2 -2
- package/infrastructure/infrastructure-composer.js +2 -0
- package/infrastructure/infrastructure-composer.test.js +2 -2
- package/infrastructure/jest.config.js +16 -0
- package/management-ui/README.md +245 -109
- package/package.json +8 -7
- package/frigg-cli/install-command/logger.js +0 -12
package/frigg-cli/validate-command/__tests__/infrastructure/integration-class-validator.test.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const { IntegrationClassValidator } = require('../../infrastructure/validators/integration-class-validator');
|
|
2
|
+
|
|
3
|
+
describe('IntegrationClassValidator', () => {
|
|
4
|
+
let validator;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
validator = new IntegrationClassValidator();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('valid integration classes', () => {
|
|
11
|
+
it('validates class with Definition static property', () => {
|
|
12
|
+
class ValidIntegration {
|
|
13
|
+
static Definition = {
|
|
14
|
+
name: 'test-integration',
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
modules: {}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const result = validator.validate(ValidIntegration, 0);
|
|
20
|
+
expect(result.isValid()).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('validates class with modules configuration', () => {
|
|
24
|
+
class ValidIntegration {
|
|
25
|
+
static Definition = {
|
|
26
|
+
name: 'oauth-integration',
|
|
27
|
+
version: '1.0.0',
|
|
28
|
+
modules: {
|
|
29
|
+
hubspot: {
|
|
30
|
+
definition: { moduleName: 'hubspot' },
|
|
31
|
+
options: {}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const result = validator.validate(ValidIntegration, 0);
|
|
37
|
+
expect(result.isValid()).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('invalid integration classes', () => {
|
|
42
|
+
it('errors when not a function/class', () => {
|
|
43
|
+
const notAClass = { Definition: { name: 'test' } };
|
|
44
|
+
const result = validator.validate(notAClass, 0);
|
|
45
|
+
expect(result.isValid()).toBe(false);
|
|
46
|
+
expect(result.getErrors()[0].message).toContain('class');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('errors when Definition is missing', () => {
|
|
50
|
+
class NoDefinition {}
|
|
51
|
+
const result = validator.validate(NoDefinition, 0);
|
|
52
|
+
expect(result.isValid()).toBe(false);
|
|
53
|
+
expect(result.getErrors()[0].path).toBe('integrations[0].Definition');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('errors when Definition.name is missing', () => {
|
|
57
|
+
class MissingName {
|
|
58
|
+
static Definition = { version: '1.0.0' };
|
|
59
|
+
}
|
|
60
|
+
const result = validator.validate(MissingName, 0);
|
|
61
|
+
expect(result.isValid()).toBe(false);
|
|
62
|
+
expect(result.getErrors()[0].path).toBe('integrations[0].Definition');
|
|
63
|
+
expect(result.getErrors()[0].message).toContain('name');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('errors when Definition.name is not a string', () => {
|
|
67
|
+
class BadName {
|
|
68
|
+
static Definition = { name: 123, version: '1.0.0' };
|
|
69
|
+
}
|
|
70
|
+
const result = validator.validate(BadName, 0);
|
|
71
|
+
expect(result.isValid()).toBe(false);
|
|
72
|
+
expect(result.getErrors()[0].path).toBe('integrations[0].Definition.name');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('modules validation', () => {
|
|
77
|
+
it('errors when module lacks definition', () => {
|
|
78
|
+
class BadModule {
|
|
79
|
+
static Definition = {
|
|
80
|
+
name: 'test',
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
modules: {
|
|
83
|
+
hubspot: { options: {} }
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const result = validator.validate(BadModule, 0);
|
|
88
|
+
expect(result.isValid()).toBe(false);
|
|
89
|
+
expect(result.getErrors().some(e => e.path.includes('modules.hubspot') && e.message.includes('definition'))).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('warns when module definition lacks moduleName', () => {
|
|
93
|
+
class ModuleNoModuleName {
|
|
94
|
+
static Definition = {
|
|
95
|
+
name: 'test',
|
|
96
|
+
version: '1.0.0',
|
|
97
|
+
modules: {
|
|
98
|
+
hubspot: {
|
|
99
|
+
definition: {},
|
|
100
|
+
options: {}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const result = validator.validate(ModuleNoModuleName, 0);
|
|
106
|
+
expect(result.getWarnings().some(w =>
|
|
107
|
+
w.code === 'MISSING_MODULE_NAME' && w.message.includes('moduleName')
|
|
108
|
+
)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('lifecycle methods', () => {
|
|
113
|
+
it('warns when onCreate is not implemented', () => {
|
|
114
|
+
class NoOnCreate {
|
|
115
|
+
static Definition = { name: 'test-integration', version: '1.0.0' };
|
|
116
|
+
}
|
|
117
|
+
const result = validator.validate(NoOnCreate, 0);
|
|
118
|
+
expect(result.getWarnings().some(w => w.message.includes('onCreate'))).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('passes when onCreate is implemented', () => {
|
|
122
|
+
class WithOnCreate {
|
|
123
|
+
static Definition = { name: 'test-integration', version: '1.0.0' };
|
|
124
|
+
async onCreate() {}
|
|
125
|
+
}
|
|
126
|
+
const result = validator.validate(WithOnCreate, 0);
|
|
127
|
+
expect(result.getWarnings().filter(w => w.message.includes('onCreate'))).toHaveLength(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('warns when getConfigOptions is not implemented', () => {
|
|
131
|
+
class NoConfigOptions {
|
|
132
|
+
static Definition = { name: 'test-integration', version: '1.0.0' };
|
|
133
|
+
}
|
|
134
|
+
const result = validator.validate(NoConfigOptions, 0);
|
|
135
|
+
expect(result.getWarnings().some(w => w.message.includes('getConfigOptions'))).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('index parameter', () => {
|
|
140
|
+
it('includes correct index in error paths', () => {
|
|
141
|
+
class Invalid {}
|
|
142
|
+
const result = validator.validate(Invalid, 5);
|
|
143
|
+
expect(result.getErrors()[0].path).toContain('integrations[5]');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Validation Tests
|
|
3
|
+
*
|
|
4
|
+
* Validates that the frigg init templates produce valid configurations
|
|
5
|
+
* when run through the validator.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { AppDefinitionValidator } = require('../../infrastructure/validators/app-definition-validator');
|
|
10
|
+
const { IntegrationClassValidator } = require('../../infrastructure/validators/integration-class-validator');
|
|
11
|
+
const { ApiModuleValidator } = require('../../infrastructure/validators/api-module-validator');
|
|
12
|
+
|
|
13
|
+
// Resolve template paths relative to frigg-cli package root
|
|
14
|
+
const FRIGG_CLI_ROOT = path.resolve(__dirname, '../../..');
|
|
15
|
+
const TEMPLATES_DIR = path.join(FRIGG_CLI_ROOT, 'templates');
|
|
16
|
+
|
|
17
|
+
describe('Template Validation', () => {
|
|
18
|
+
let appDefinitionValidator;
|
|
19
|
+
let integrationClassValidator;
|
|
20
|
+
let apiModuleValidator;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
appDefinitionValidator = new AppDefinitionValidator();
|
|
24
|
+
integrationClassValidator = new IntegrationClassValidator();
|
|
25
|
+
apiModuleValidator = new ApiModuleValidator();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('backend/index.js template', () => {
|
|
29
|
+
// Load the actual template
|
|
30
|
+
const templatePath = path.join(TEMPLATES_DIR, 'backend/index.js');
|
|
31
|
+
let templateModule;
|
|
32
|
+
|
|
33
|
+
beforeAll(() => {
|
|
34
|
+
// Clear require cache to ensure fresh load
|
|
35
|
+
delete require.cache[require.resolve(templatePath)];
|
|
36
|
+
templateModule = require(templatePath);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('exports a Definition property', () => {
|
|
40
|
+
expect(templateModule.Definition).toBeDefined();
|
|
41
|
+
expect(typeof templateModule.Definition).toBe('object');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('has valid app definition structure', () => {
|
|
45
|
+
const result = appDefinitionValidator.validate(templateModule.Definition);
|
|
46
|
+
|
|
47
|
+
// Log errors for debugging
|
|
48
|
+
if (!result.isValid()) {
|
|
49
|
+
console.log('App Definition Errors:', result.getErrors().map(e => `${e.path}: ${e.message}`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
expect(result.isValid()).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('has required integrations array', () => {
|
|
56
|
+
expect(templateModule.Definition.integrations).toBeDefined();
|
|
57
|
+
expect(Array.isArray(templateModule.Definition.integrations)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('has valid user configuration', () => {
|
|
61
|
+
const userConfig = templateModule.Definition.user;
|
|
62
|
+
expect(userConfig).toBeDefined();
|
|
63
|
+
expect(typeof userConfig.usePassword).toBe('boolean');
|
|
64
|
+
expect(['individual', 'organization']).toContain(userConfig.primary);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('has valid database configuration', () => {
|
|
68
|
+
const dbConfig = templateModule.Definition.database;
|
|
69
|
+
expect(dbConfig).toBeDefined();
|
|
70
|
+
// Should have at least one database type configured
|
|
71
|
+
expect(dbConfig.postgres || dbConfig.mongoDB || dbConfig.documentDB).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('has valid vpc configuration', () => {
|
|
75
|
+
const vpcConfig = templateModule.Definition.vpc;
|
|
76
|
+
expect(vpcConfig).toBeDefined();
|
|
77
|
+
expect(typeof vpcConfig.enable).toBe('boolean');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('has valid encryption configuration', () => {
|
|
81
|
+
const encConfig = templateModule.Definition.encryption;
|
|
82
|
+
expect(encConfig).toBeDefined();
|
|
83
|
+
expect(['kms', 'aes', 'none']).toContain(encConfig.fieldLevelEncryptionMethod);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('ExampleIntegration template', () => {
|
|
88
|
+
const templatePath = path.join(TEMPLATES_DIR, 'backend/src/integrations/ExampleIntegration.js');
|
|
89
|
+
let ExampleIntegration;
|
|
90
|
+
|
|
91
|
+
beforeAll(() => {
|
|
92
|
+
// Mock @friggframework/core since it may not be installed in CLI package
|
|
93
|
+
jest.mock('@friggframework/core', () => ({
|
|
94
|
+
Integration: class Integration {
|
|
95
|
+
static Config = {};
|
|
96
|
+
}
|
|
97
|
+
}), { virtual: true });
|
|
98
|
+
|
|
99
|
+
delete require.cache[require.resolve(templatePath)];
|
|
100
|
+
ExampleIntegration = require(templatePath);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterAll(() => {
|
|
104
|
+
jest.unmock('@friggframework/core');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('is a class/function', () => {
|
|
108
|
+
expect(typeof ExampleIntegration).toBe('function');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('has static Definition property', () => {
|
|
112
|
+
expect(ExampleIntegration.Definition).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('Definition has required properties', () => {
|
|
116
|
+
const definition = ExampleIntegration.Definition;
|
|
117
|
+
expect(definition.name).toBeDefined();
|
|
118
|
+
expect(definition.version).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('Definition matches pattern used in core integrations', () => {
|
|
122
|
+
// ExampleIntegration uses static Definition (not Config)
|
|
123
|
+
// to match the pattern expected by IntegrationClassValidator
|
|
124
|
+
expect(ExampleIntegration.Definition).toBeDefined();
|
|
125
|
+
expect(typeof ExampleIntegration.Definition.name).toBe('string');
|
|
126
|
+
expect(typeof ExampleIntegration.Definition.version).toBe('string');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('Template schema compliance', () => {
|
|
131
|
+
it('template app definition does not use unknown properties', () => {
|
|
132
|
+
const templatePath = path.join(TEMPLATES_DIR, 'backend/index.js');
|
|
133
|
+
delete require.cache[require.resolve(templatePath)];
|
|
134
|
+
const { Definition } = require(templatePath);
|
|
135
|
+
|
|
136
|
+
const result = appDefinitionValidator.validate(Definition);
|
|
137
|
+
const errors = result.getErrors();
|
|
138
|
+
|
|
139
|
+
// Check for "additional property" errors which indicate unknown fields
|
|
140
|
+
const additionalPropErrors = errors.filter(e =>
|
|
141
|
+
e.message.includes('additional properties') ||
|
|
142
|
+
e.code === 'ADDITIONALPROPERTIES'
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (additionalPropErrors.length > 0) {
|
|
146
|
+
console.log('Unknown properties in template:',
|
|
147
|
+
additionalPropErrors.map(e => e.message));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// For now, document what additional properties exist
|
|
151
|
+
// These may need to be added to the schema or removed from template
|
|
152
|
+
expect(additionalPropErrors).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { ValidateAppUseCase } = require('../../application/use-cases/validate-app-use-case');
|
|
4
|
+
const { AppDefinitionValidator } = require('../../infrastructure/validators/app-definition-validator');
|
|
5
|
+
const { IntegrationClassValidator } = require('../../infrastructure/validators/integration-class-validator');
|
|
6
|
+
const { ApiModuleValidator } = require('../../infrastructure/validators/api-module-validator');
|
|
7
|
+
|
|
8
|
+
function createValidateCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('validate [path]')
|
|
11
|
+
.description('Validate a Frigg application configuration (auto-detects local app if no path given)')
|
|
12
|
+
.option('-f, --format <format>', 'Output format (console, json)', 'console')
|
|
13
|
+
.option('-v, --verbose', 'Show detailed output including fix suggestions', false)
|
|
14
|
+
.action(async (appPath, options) => {
|
|
15
|
+
const output = require('../../../utils/output');
|
|
16
|
+
await validateCommand(appPath, options, { output });
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function autoDetectFriggApp(startDir = process.cwd()) {
|
|
21
|
+
let currentDir = startDir;
|
|
22
|
+
const root = path.parse(currentDir).root;
|
|
23
|
+
|
|
24
|
+
while (currentDir !== root) {
|
|
25
|
+
const backendPath = findBackendPathInDir(currentDir);
|
|
26
|
+
if (backendPath) {
|
|
27
|
+
return { appRoot: currentDir, backendPath };
|
|
28
|
+
}
|
|
29
|
+
currentDir = path.dirname(currentDir);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function findBackendPathInDir(dir) {
|
|
36
|
+
const backendDir = path.join(dir, 'backend');
|
|
37
|
+
if (fs.existsSync(path.join(backendDir, 'index.js'))) {
|
|
38
|
+
return backendDir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (fs.existsSync(path.join(dir, 'index.js'))) {
|
|
42
|
+
const content = fs.readFileSync(path.join(dir, 'index.js'), 'utf-8');
|
|
43
|
+
if (content.includes('integrations') || content.includes('Definition')) {
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findBackendPath(startPath) {
|
|
52
|
+
const absolutePath = path.isAbsolute(startPath) ? startPath : path.resolve(process.cwd(), startPath);
|
|
53
|
+
|
|
54
|
+
const backendDir = path.join(absolutePath, 'backend');
|
|
55
|
+
if (fs.existsSync(path.join(backendDir, 'package.json')) || fs.existsSync(path.join(backendDir, 'index.js'))) {
|
|
56
|
+
return backendDir;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (fs.existsSync(path.join(absolutePath, 'package.json')) || fs.existsSync(path.join(absolutePath, 'index.js'))) {
|
|
60
|
+
return absolutePath;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadAppDefinition(backendPath) {
|
|
67
|
+
const indexPath = path.join(backendPath, 'index.js');
|
|
68
|
+
if (!fs.existsSync(indexPath)) {
|
|
69
|
+
throw new Error(`No index.js found at ${backendPath}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const originalCwd = process.cwd();
|
|
73
|
+
try {
|
|
74
|
+
process.chdir(backendPath);
|
|
75
|
+
delete require.cache[require.resolve(indexPath)];
|
|
76
|
+
const exported = require(indexPath);
|
|
77
|
+
return exported.Definition || exported.default || exported;
|
|
78
|
+
} finally {
|
|
79
|
+
process.chdir(originalCwd);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function validateCommand(appPath, options, { output }) {
|
|
84
|
+
try {
|
|
85
|
+
let backendPath;
|
|
86
|
+
let definition;
|
|
87
|
+
|
|
88
|
+
if (!appPath) {
|
|
89
|
+
const detected = autoDetectFriggApp();
|
|
90
|
+
if (!detected) {
|
|
91
|
+
output.error('No Frigg app found. Run from within a Frigg app directory or specify a path.');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
backendPath = detected.backendPath;
|
|
95
|
+
output.info(`Auto-detected Frigg app at: ${detected.appRoot}`);
|
|
96
|
+
} else {
|
|
97
|
+
backendPath = findBackendPath(appPath);
|
|
98
|
+
if (!backendPath) {
|
|
99
|
+
output.error(`Could not find backend directory in ${appPath}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
output.info(`Validating Frigg app at: ${appPath}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
definition = loadAppDefinition(backendPath);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
output.error(`Could not load app definition: ${err.message}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const appDefinitionValidator = new AppDefinitionValidator();
|
|
113
|
+
const integrationClassValidator = new IntegrationClassValidator();
|
|
114
|
+
const apiModuleValidator = new ApiModuleValidator();
|
|
115
|
+
const useCase = new ValidateAppUseCase({
|
|
116
|
+
appDefinitionValidator,
|
|
117
|
+
integrationClassValidator,
|
|
118
|
+
apiModuleValidator
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const result = await useCase.execute({ definition, appPath: backendPath });
|
|
122
|
+
|
|
123
|
+
if (options.format === 'json') {
|
|
124
|
+
output.log(JSON.stringify(result.toJSON(), null, 2));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
formatConsoleOutput(result, options, output);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
output.error(`Validation failed: ${err.message}`);
|
|
131
|
+
if (options.verbose) {
|
|
132
|
+
output.error(err.stack);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatConsoleOutput(result, options, output) {
|
|
138
|
+
const summary = result.getSummary();
|
|
139
|
+
|
|
140
|
+
output.log('');
|
|
141
|
+
output.log('═'.repeat(60));
|
|
142
|
+
output.log(' FRIGG VALIDATE - App Configuration Report');
|
|
143
|
+
output.log('═'.repeat(60));
|
|
144
|
+
output.log('');
|
|
145
|
+
|
|
146
|
+
if (result.isValid()) {
|
|
147
|
+
output.success('App configuration is valid');
|
|
148
|
+
} else {
|
|
149
|
+
output.error(`App configuration has ${summary.errorCount} error(s)`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (summary.warningCount > 0) {
|
|
153
|
+
output.warn(`${summary.warningCount} warning(s) found`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
output.log('');
|
|
157
|
+
|
|
158
|
+
if (result.getErrors().length > 0) {
|
|
159
|
+
output.log('─'.repeat(60));
|
|
160
|
+
output.log('ERRORS:');
|
|
161
|
+
output.log('');
|
|
162
|
+
result.getErrors().forEach((error, idx) => {
|
|
163
|
+
output.log(` ${idx + 1}. [${error.path}] ${error.message}`);
|
|
164
|
+
if (options.verbose && error.fix) {
|
|
165
|
+
output.log(` Fix: ${error.fix.description}`);
|
|
166
|
+
if (error.fix.template) {
|
|
167
|
+
output.log(` Template: ${JSON.stringify(error.fix.template)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
output.log('');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (result.getWarnings().length > 0) {
|
|
175
|
+
output.log('─'.repeat(60));
|
|
176
|
+
output.log('WARNINGS:');
|
|
177
|
+
output.log('');
|
|
178
|
+
result.getWarnings().forEach((warning, idx) => {
|
|
179
|
+
output.log(` ${idx + 1}. [${warning.path}] ${warning.message}`);
|
|
180
|
+
if (options.verbose && warning.fix) {
|
|
181
|
+
output.log(` Fix: ${warning.fix.description}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
output.log('');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
output.log('═'.repeat(60));
|
|
188
|
+
output.log('');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
validateCommand,
|
|
193
|
+
createValidateCommand,
|
|
194
|
+
formatConsoleOutput,
|
|
195
|
+
autoDetectFriggApp,
|
|
196
|
+
findBackendPathInDir,
|
|
197
|
+
findBackendPath,
|
|
198
|
+
loadAppDefinition
|
|
199
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { ValidationResult } = require('../../domain/entities/validation-result');
|
|
2
|
+
|
|
3
|
+
class ValidateAppUseCase {
|
|
4
|
+
constructor({ appDefinitionValidator, integrationClassValidator, apiModuleValidator }) {
|
|
5
|
+
this.appDefinitionValidator = appDefinitionValidator;
|
|
6
|
+
this.integrationClassValidator = integrationClassValidator;
|
|
7
|
+
this.apiModuleValidator = apiModuleValidator;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async execute({ definition, appPath }) {
|
|
11
|
+
let result = ValidationResult.create({
|
|
12
|
+
context: { appPath }
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const appResult = this.appDefinitionValidator.validate(definition);
|
|
16
|
+
result = result.merge(appResult);
|
|
17
|
+
|
|
18
|
+
if (definition.integrations && Array.isArray(definition.integrations)) {
|
|
19
|
+
definition.integrations.forEach((integration, index) => {
|
|
20
|
+
const integrationResult = this.integrationClassValidator.validate(integration, index);
|
|
21
|
+
result = result.merge(integrationResult);
|
|
22
|
+
|
|
23
|
+
// Validate API modules within the integration
|
|
24
|
+
if (this.apiModuleValidator && integration.Definition) {
|
|
25
|
+
const moduleResult = this.apiModuleValidator.validate(integration.Definition, index);
|
|
26
|
+
result = result.merge(moduleResult);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { ValidateAppUseCase };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
class ValidationResult {
|
|
2
|
+
constructor({ errors, context }) {
|
|
3
|
+
this.errors = errors || [];
|
|
4
|
+
this.context = context || {};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
static create(props = {}) {
|
|
8
|
+
return new ValidationResult(props);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
isValid() {
|
|
12
|
+
return !this.errors.some(e => e.isError());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getErrors() {
|
|
16
|
+
return this.errors.filter(e => e.isError());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getWarnings() {
|
|
20
|
+
return this.errors.filter(e => e.isWarning());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getInfos() {
|
|
24
|
+
return this.errors.filter(e => e.isInfo());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
addError(error) {
|
|
28
|
+
this.errors.push(error);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
merge(other) {
|
|
33
|
+
return ValidationResult.create({
|
|
34
|
+
errors: [...this.errors, ...other.errors],
|
|
35
|
+
context: { ...this.context, ...other.context }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getContext() {
|
|
40
|
+
return this.context;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
filterByPath(pathPrefix) {
|
|
44
|
+
return ValidationResult.create({
|
|
45
|
+
errors: this.errors.filter(e => e.path.startsWith(pathPrefix)),
|
|
46
|
+
context: this.context
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getBySeverity(severity) {
|
|
51
|
+
return this.errors.filter(e => e.severity === severity);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getSummary() {
|
|
55
|
+
return {
|
|
56
|
+
isValid: this.isValid(),
|
|
57
|
+
errorCount: this.getErrors().length,
|
|
58
|
+
warningCount: this.getWarnings().length,
|
|
59
|
+
infoCount: this.getInfos().length,
|
|
60
|
+
totalCount: this.errors.length
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
toJSON() {
|
|
65
|
+
return {
|
|
66
|
+
valid: this.isValid(),
|
|
67
|
+
errors: this.errors.map(e => e.toJSON()),
|
|
68
|
+
summary: this.getSummary(),
|
|
69
|
+
context: this.context
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { ValidationResult };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const VALID_ACTIONS = ['add', 'remove', 'replace', 'rename', 'update'];
|
|
2
|
+
|
|
3
|
+
class FixSuggestion {
|
|
4
|
+
constructor({ action, description, template, codeSnippet, targetPath, targetFile }) {
|
|
5
|
+
if (!action) {
|
|
6
|
+
throw new Error('action is required');
|
|
7
|
+
}
|
|
8
|
+
if (!description) {
|
|
9
|
+
throw new Error('description is required');
|
|
10
|
+
}
|
|
11
|
+
if (!VALID_ACTIONS.includes(action)) {
|
|
12
|
+
throw new Error(`Invalid action: ${action}. Must be one of: ${VALID_ACTIONS.join(', ')}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
this.action = action;
|
|
16
|
+
this.description = description;
|
|
17
|
+
this.template = template || null;
|
|
18
|
+
this.codeSnippet = codeSnippet || null;
|
|
19
|
+
this.targetPath = targetPath || null;
|
|
20
|
+
this.targetFile = targetFile || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static create(props) {
|
|
24
|
+
return new FixSuggestion(props);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
isAdd() {
|
|
28
|
+
return this.action === 'add';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
isRemove() {
|
|
32
|
+
return this.action === 'remove';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isReplace() {
|
|
36
|
+
return this.action === 'replace';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
isRename() {
|
|
40
|
+
return this.action === 'rename';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
isUpdate() {
|
|
44
|
+
return this.action === 'update';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
isAutoApplicable() {
|
|
48
|
+
return this.template !== null || this.codeSnippet !== null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
toJSON() {
|
|
52
|
+
return {
|
|
53
|
+
action: this.action,
|
|
54
|
+
description: this.description,
|
|
55
|
+
template: this.template,
|
|
56
|
+
codeSnippet: this.codeSnippet,
|
|
57
|
+
targetPath: this.targetPath,
|
|
58
|
+
targetFile: this.targetFile
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
format() {
|
|
63
|
+
let output = `[${this.action}] ${this.description}`;
|
|
64
|
+
if (this.template) {
|
|
65
|
+
output += `\n Template: ${JSON.stringify(this.template)}`;
|
|
66
|
+
}
|
|
67
|
+
if (this.codeSnippet) {
|
|
68
|
+
output += `\n Code: ${this.codeSnippet}`;
|
|
69
|
+
}
|
|
70
|
+
return output;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { FixSuggestion };
|