@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
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
const {DomainException} = require('../exceptions/DomainException');
|
|
2
|
+
const {SemanticVersion} = require('../value-objects/SemanticVersion');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ApiModule Entity
|
|
6
|
+
*
|
|
7
|
+
* Represents an API module that can be used by integrations
|
|
8
|
+
* API modules are reusable API clients for external services
|
|
9
|
+
*/
|
|
10
|
+
class ApiModule {
|
|
11
|
+
constructor(props) {
|
|
12
|
+
this.name = props.name; // kebab-case name
|
|
13
|
+
this.version = props.version instanceof SemanticVersion ?
|
|
14
|
+
props.version : new SemanticVersion(props.version || '1.0.0');
|
|
15
|
+
this.displayName = props.displayName || this._generateDisplayName();
|
|
16
|
+
this.description = props.description || '';
|
|
17
|
+
this.author = props.author || '';
|
|
18
|
+
this.license = props.license || 'UNLICENSED';
|
|
19
|
+
this.apiConfig = props.apiConfig || {
|
|
20
|
+
baseUrl: '',
|
|
21
|
+
authType: 'oauth2',
|
|
22
|
+
version: 'v1'
|
|
23
|
+
};
|
|
24
|
+
this.entities = props.entities || {}; // Database entities this module needs
|
|
25
|
+
this.scopes = props.scopes || []; // OAuth scopes required
|
|
26
|
+
this.credentials = props.credentials || []; // Required credentials
|
|
27
|
+
this.endpoints = props.endpoints || {}; // API endpoints
|
|
28
|
+
this.createdAt = props.createdAt || new Date();
|
|
29
|
+
this.updatedAt = props.updatedAt || new Date();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Factory method to create a new ApiModule
|
|
34
|
+
*/
|
|
35
|
+
static create(props) {
|
|
36
|
+
if (!props.name) {
|
|
37
|
+
throw new DomainException('API module name is required');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate name format
|
|
41
|
+
const namePattern = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
42
|
+
if (!namePattern.test(props.name)) {
|
|
43
|
+
throw new DomainException('API module name must be kebab-case');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Validate authType is provided
|
|
47
|
+
if (!props.apiConfig || !props.apiConfig.authType) {
|
|
48
|
+
throw new DomainException('Authentication type is required');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return new ApiModule(props);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reconstruct ApiModule from plain object
|
|
56
|
+
*/
|
|
57
|
+
static fromObject(obj) {
|
|
58
|
+
return new ApiModule({
|
|
59
|
+
...obj,
|
|
60
|
+
version: obj.version,
|
|
61
|
+
createdAt: new Date(obj.createdAt),
|
|
62
|
+
updatedAt: new Date(obj.updatedAt)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Add an entity configuration
|
|
68
|
+
* Entities are database records that store API credentials and state
|
|
69
|
+
*
|
|
70
|
+
* @param {string} entityName - Entity name (e.g., 'credential', 'user')
|
|
71
|
+
* @param {object} config - Entity configuration
|
|
72
|
+
*/
|
|
73
|
+
addEntity(entityName, config = {}) {
|
|
74
|
+
if (this.hasEntity(entityName)) {
|
|
75
|
+
throw new DomainException(`Entity '${entityName}' already exists`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.entities[entityName] = {
|
|
79
|
+
type: entityName,
|
|
80
|
+
label: config.label || entityName,
|
|
81
|
+
required: config.required !== false,
|
|
82
|
+
fields: config.fields || [],
|
|
83
|
+
...config
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
this.updatedAt = new Date();
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if entity exists
|
|
92
|
+
*/
|
|
93
|
+
hasEntity(entityName) {
|
|
94
|
+
return entityName in this.entities;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Add an endpoint definition
|
|
99
|
+
*/
|
|
100
|
+
addEndpoint(name, config) {
|
|
101
|
+
if (this.hasEndpoint(name)) {
|
|
102
|
+
throw new DomainException(`Endpoint '${name}' already exists`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.endpoints[name] = {
|
|
106
|
+
method: config.method || 'GET',
|
|
107
|
+
path: config.path,
|
|
108
|
+
description: config.description || '',
|
|
109
|
+
parameters: config.parameters || [],
|
|
110
|
+
response: config.response || {},
|
|
111
|
+
...config
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
this.updatedAt = new Date();
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if endpoint exists
|
|
120
|
+
*/
|
|
121
|
+
hasEndpoint(name) {
|
|
122
|
+
return name in this.endpoints;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Add required OAuth scope
|
|
127
|
+
*/
|
|
128
|
+
addScope(scope) {
|
|
129
|
+
if (this.scopes.includes(scope)) {
|
|
130
|
+
throw new DomainException(`Scope '${scope}' already exists`);
|
|
131
|
+
}
|
|
132
|
+
this.scopes.push(scope);
|
|
133
|
+
this.updatedAt = new Date();
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Add required credential
|
|
139
|
+
*/
|
|
140
|
+
addCredential(name, config = {}) {
|
|
141
|
+
const existing = this.credentials.find(c => c.name === name);
|
|
142
|
+
if (existing) {
|
|
143
|
+
throw new DomainException(`Credential '${name}' already exists`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.credentials.push({
|
|
147
|
+
name,
|
|
148
|
+
type: config.type || 'string',
|
|
149
|
+
required: config.required !== false,
|
|
150
|
+
description: config.description || '',
|
|
151
|
+
example: config.example || '',
|
|
152
|
+
envVar: config.envVar || '',
|
|
153
|
+
...config
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
this.updatedAt = new Date();
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if credential exists
|
|
162
|
+
*/
|
|
163
|
+
hasCredential(name) {
|
|
164
|
+
return this.credentials.some(c => c.name === name);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Validate API module business rules
|
|
169
|
+
*/
|
|
170
|
+
validate() {
|
|
171
|
+
const errors = [];
|
|
172
|
+
|
|
173
|
+
// Name validation (kebab-case)
|
|
174
|
+
if (!this.name || this.name.trim().length === 0) {
|
|
175
|
+
errors.push('API module name is required');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const namePattern = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
179
|
+
if (this.name && !namePattern.test(this.name)) {
|
|
180
|
+
errors.push('API module name must be kebab-case');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Display name validation
|
|
184
|
+
if (!this.displayName || this.displayName.trim().length === 0) {
|
|
185
|
+
errors.push('Display name is required');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Description validation
|
|
189
|
+
if (this.description && this.description.length > 1000) {
|
|
190
|
+
errors.push('Description must be 1000 characters or less');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// API config validation
|
|
194
|
+
if (!this.apiConfig.baseUrl) {
|
|
195
|
+
// Warning: base URL should be provided, but not required at creation
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Auth type validation
|
|
199
|
+
if (!this.apiConfig.authType || this.apiConfig.authType.trim().length === 0) {
|
|
200
|
+
errors.push('Authentication type is required');
|
|
201
|
+
} else {
|
|
202
|
+
const validAuthTypes = ['oauth2', 'api-key', 'basic', 'token', 'custom'];
|
|
203
|
+
if (!validAuthTypes.includes(this.apiConfig.authType)) {
|
|
204
|
+
errors.push(`Invalid auth type. Must be one of: ${validAuthTypes.join(', ')}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
isValid: errors.length === 0,
|
|
210
|
+
errors
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Convert to plain object
|
|
216
|
+
*/
|
|
217
|
+
toObject() {
|
|
218
|
+
return {
|
|
219
|
+
name: this.name,
|
|
220
|
+
version: this.version.value,
|
|
221
|
+
displayName: this.displayName,
|
|
222
|
+
description: this.description,
|
|
223
|
+
author: this.author,
|
|
224
|
+
license: this.license,
|
|
225
|
+
apiConfig: this.apiConfig,
|
|
226
|
+
entities: this.entities,
|
|
227
|
+
scopes: this.scopes,
|
|
228
|
+
credentials: this.credentials,
|
|
229
|
+
endpoints: this.endpoints,
|
|
230
|
+
createdAt: this.createdAt,
|
|
231
|
+
updatedAt: this.updatedAt
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Convert to JSON format (for api-module definition files)
|
|
237
|
+
*/
|
|
238
|
+
toJSON() {
|
|
239
|
+
return {
|
|
240
|
+
name: this.name,
|
|
241
|
+
version: this.version.value,
|
|
242
|
+
display: {
|
|
243
|
+
name: this.displayName,
|
|
244
|
+
description: this.description
|
|
245
|
+
},
|
|
246
|
+
api: {
|
|
247
|
+
baseUrl: this.apiConfig.baseUrl,
|
|
248
|
+
authType: this.apiConfig.authType,
|
|
249
|
+
version: this.apiConfig.version
|
|
250
|
+
},
|
|
251
|
+
entities: this.entities,
|
|
252
|
+
auth: {
|
|
253
|
+
type: this.apiConfig.authType,
|
|
254
|
+
scopes: this.scopes,
|
|
255
|
+
credentials: this.credentials
|
|
256
|
+
},
|
|
257
|
+
endpoints: this.endpoints
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Generate display name from kebab-case name
|
|
263
|
+
*/
|
|
264
|
+
_generateDisplayName() {
|
|
265
|
+
return this.name
|
|
266
|
+
.split('-')
|
|
267
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
268
|
+
.join(' ');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {ApiModule};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const {DomainException} = require('../exceptions/DomainException');
|
|
2
|
+
const {SemanticVersion} = require('../value-objects/SemanticVersion');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AppDefinition Aggregate Root
|
|
6
|
+
*
|
|
7
|
+
* Represents the entire Frigg application configuration
|
|
8
|
+
* Contains metadata about the app and references to all integrations
|
|
9
|
+
*/
|
|
10
|
+
class AppDefinition {
|
|
11
|
+
constructor(props) {
|
|
12
|
+
this.name = props.name;
|
|
13
|
+
this.version = props.version instanceof SemanticVersion ?
|
|
14
|
+
props.version : new SemanticVersion(props.version);
|
|
15
|
+
this.description = props.description || '';
|
|
16
|
+
this.author = props.author || '';
|
|
17
|
+
this.license = props.license || 'UNLICENSED';
|
|
18
|
+
this.repository = props.repository || {};
|
|
19
|
+
this.integrations = props.integrations || [];
|
|
20
|
+
this.apiModules = props.apiModules || [];
|
|
21
|
+
this.config = props.config || {};
|
|
22
|
+
this.createdAt = props.createdAt || new Date();
|
|
23
|
+
this.updatedAt = props.updatedAt || new Date();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Factory method to create a new AppDefinition
|
|
28
|
+
*/
|
|
29
|
+
static create(props) {
|
|
30
|
+
return new AppDefinition(props);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Register an integration in the app
|
|
35
|
+
* @param {string} integrationName - Name of the integration to register
|
|
36
|
+
*/
|
|
37
|
+
registerIntegration(integrationName) {
|
|
38
|
+
if (this.hasIntegration(integrationName)) {
|
|
39
|
+
throw new DomainException(`Integration '${integrationName}' is already registered`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.integrations.push({
|
|
43
|
+
name: integrationName,
|
|
44
|
+
enabled: true,
|
|
45
|
+
registeredAt: new Date()
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.updatedAt = new Date();
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Unregister an integration from the app
|
|
54
|
+
* @param {string} integrationName
|
|
55
|
+
*/
|
|
56
|
+
unregisterIntegration(integrationName) {
|
|
57
|
+
const index = this.integrations.findIndex(i => i.name === integrationName);
|
|
58
|
+
|
|
59
|
+
if (index === -1) {
|
|
60
|
+
throw new DomainException(`Integration '${integrationName}' is not registered`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.integrations.splice(index, 1);
|
|
64
|
+
this.updatedAt = new Date();
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if an integration is registered
|
|
70
|
+
* @param {string} integrationName
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
hasIntegration(integrationName) {
|
|
74
|
+
return this.integrations.some(i => i.name === integrationName);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Enable an integration
|
|
79
|
+
* @param {string} integrationName
|
|
80
|
+
*/
|
|
81
|
+
enableIntegration(integrationName) {
|
|
82
|
+
const integration = this.integrations.find(i => i.name === integrationName);
|
|
83
|
+
|
|
84
|
+
if (!integration) {
|
|
85
|
+
throw new DomainException(`Integration '${integrationName}' is not registered`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
integration.enabled = true;
|
|
89
|
+
this.updatedAt = new Date();
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Disable an integration
|
|
95
|
+
* @param {string} integrationName
|
|
96
|
+
*/
|
|
97
|
+
disableIntegration(integrationName) {
|
|
98
|
+
const integration = this.integrations.find(i => i.name === integrationName);
|
|
99
|
+
|
|
100
|
+
if (!integration) {
|
|
101
|
+
throw new DomainException(`Integration '${integrationName}' is not registered`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
integration.enabled = false;
|
|
105
|
+
this.updatedAt = new Date();
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Register an API module
|
|
111
|
+
* @param {string} moduleName
|
|
112
|
+
* @param {string} moduleVersion
|
|
113
|
+
* @param {string} source - npm, local, git
|
|
114
|
+
*/
|
|
115
|
+
registerApiModule(moduleName, moduleVersion, source = 'npm') {
|
|
116
|
+
if (this.hasApiModule(moduleName)) {
|
|
117
|
+
throw new DomainException(`API module '${moduleName}' is already registered`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.apiModules.push({
|
|
121
|
+
name: moduleName,
|
|
122
|
+
version: moduleVersion,
|
|
123
|
+
source,
|
|
124
|
+
registeredAt: new Date()
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
this.updatedAt = new Date();
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if an API module is registered
|
|
133
|
+
* @param {string} moduleName
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
hasApiModule(moduleName) {
|
|
137
|
+
return this.apiModules.some(m => m.name === moduleName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get all enabled integrations
|
|
142
|
+
* @returns {Array}
|
|
143
|
+
*/
|
|
144
|
+
getEnabledIntegrations() {
|
|
145
|
+
return this.integrations.filter(i => i.enabled);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Validate app definition business rules
|
|
150
|
+
*/
|
|
151
|
+
validate() {
|
|
152
|
+
const errors = [];
|
|
153
|
+
|
|
154
|
+
// Name validation
|
|
155
|
+
if (!this.name || this.name.trim().length === 0) {
|
|
156
|
+
errors.push('App name is required');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (this.name && this.name.length > 100) {
|
|
160
|
+
errors.push('App name must be 100 characters or less');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Version validation (handled by SemanticVersion value object)
|
|
164
|
+
|
|
165
|
+
// Description validation
|
|
166
|
+
if (this.description && this.description.length > 1000) {
|
|
167
|
+
errors.push('Description must be 1000 characters or less');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Integrations validation
|
|
171
|
+
const integrationNames = this.integrations.map(i => i.name);
|
|
172
|
+
const uniqueNames = new Set(integrationNames);
|
|
173
|
+
if (integrationNames.length !== uniqueNames.size) {
|
|
174
|
+
errors.push('Duplicate integration names found');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
isValid: errors.length === 0,
|
|
179
|
+
errors
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Convert to plain object
|
|
185
|
+
*/
|
|
186
|
+
toObject() {
|
|
187
|
+
return {
|
|
188
|
+
name: this.name,
|
|
189
|
+
version: this.version.value,
|
|
190
|
+
description: this.description,
|
|
191
|
+
author: this.author,
|
|
192
|
+
license: this.license,
|
|
193
|
+
repository: this.repository,
|
|
194
|
+
integrations: this.integrations,
|
|
195
|
+
apiModules: this.apiModules,
|
|
196
|
+
config: this.config,
|
|
197
|
+
createdAt: this.createdAt,
|
|
198
|
+
updatedAt: this.updatedAt
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Convert to JSON format (for app-definition.json)
|
|
204
|
+
*/
|
|
205
|
+
toJSON() {
|
|
206
|
+
return {
|
|
207
|
+
name: this.name,
|
|
208
|
+
version: this.version.value,
|
|
209
|
+
description: this.description,
|
|
210
|
+
author: this.author,
|
|
211
|
+
license: this.license,
|
|
212
|
+
repository: this.repository,
|
|
213
|
+
integrations: this.integrations.map(i => ({
|
|
214
|
+
name: i.name,
|
|
215
|
+
enabled: i.enabled
|
|
216
|
+
})),
|
|
217
|
+
apiModules: this.apiModules.map(m => ({
|
|
218
|
+
name: m.name,
|
|
219
|
+
version: m.version,
|
|
220
|
+
source: m.source
|
|
221
|
+
})),
|
|
222
|
+
config: this.config
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {AppDefinition};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const {IntegrationId} = require('../value-objects/IntegrationId');
|
|
2
|
+
const {IntegrationName} = require('../value-objects/IntegrationName');
|
|
3
|
+
const {SemanticVersion} = require('../value-objects/SemanticVersion');
|
|
4
|
+
const {DomainException} = require('../exceptions/DomainException');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Integration Aggregate Root
|
|
8
|
+
* Represents a Frigg integration with business rules
|
|
9
|
+
*/
|
|
10
|
+
class Integration {
|
|
11
|
+
constructor(props) {
|
|
12
|
+
// Value objects (immutable, self-validating)
|
|
13
|
+
this.id = props.id instanceof IntegrationId ? props.id : new IntegrationId(props.id);
|
|
14
|
+
this.name = props.name instanceof IntegrationName ? props.name : new IntegrationName(props.name);
|
|
15
|
+
this.version = props.version instanceof SemanticVersion
|
|
16
|
+
? props.version
|
|
17
|
+
: new SemanticVersion(props.version || '1.0.0');
|
|
18
|
+
|
|
19
|
+
// Simple properties
|
|
20
|
+
this.displayName = props.displayName || this._generateDisplayName();
|
|
21
|
+
this.description = props.description || '';
|
|
22
|
+
this.type = props.type || 'custom';
|
|
23
|
+
this.category = props.category;
|
|
24
|
+
this.tags = props.tags || [];
|
|
25
|
+
|
|
26
|
+
// Complex properties
|
|
27
|
+
this.entities = props.entities || {};
|
|
28
|
+
this.apiModules = props.apiModules || [];
|
|
29
|
+
this.capabilities = props.capabilities || {};
|
|
30
|
+
this.requirements = props.requirements || {};
|
|
31
|
+
this.options = props.options || {};
|
|
32
|
+
|
|
33
|
+
// Metadata
|
|
34
|
+
this.createdAt = props.createdAt || new Date();
|
|
35
|
+
this.updatedAt = props.updatedAt || new Date();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Factory method for creating new integrations
|
|
40
|
+
*/
|
|
41
|
+
static create(props) {
|
|
42
|
+
return new Integration({
|
|
43
|
+
id: IntegrationId.generate(),
|
|
44
|
+
...props,
|
|
45
|
+
createdAt: new Date(),
|
|
46
|
+
updatedAt: new Date()
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Add an API module to this integration
|
|
52
|
+
*/
|
|
53
|
+
addApiModule(moduleName, moduleVersion, source = 'npm') {
|
|
54
|
+
if (this.hasApiModule(moduleName)) {
|
|
55
|
+
throw new DomainException(`API module '${moduleName}' is already added to this integration`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.apiModules.push({
|
|
59
|
+
name: moduleName,
|
|
60
|
+
version: moduleVersion,
|
|
61
|
+
source
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.updatedAt = new Date();
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Remove an API module from this integration
|
|
70
|
+
*/
|
|
71
|
+
removeApiModule(moduleName) {
|
|
72
|
+
const index = this.apiModules.findIndex(m => m.name === moduleName);
|
|
73
|
+
if (index === -1) {
|
|
74
|
+
throw new DomainException(`API module '${moduleName}' not found in this integration`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.apiModules.splice(index, 1);
|
|
78
|
+
this.updatedAt = new Date();
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if API module is already added
|
|
84
|
+
*/
|
|
85
|
+
hasApiModule(moduleName) {
|
|
86
|
+
return this.apiModules.some(m => m.name === moduleName);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Add an entity to this integration
|
|
91
|
+
*/
|
|
92
|
+
addEntity(entityKey, entityConfig) {
|
|
93
|
+
if (this.entities[entityKey]) {
|
|
94
|
+
throw new DomainException(`Entity '${entityKey}' already exists in this integration`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.entities[entityKey] = {
|
|
98
|
+
type: entityConfig.type || entityKey,
|
|
99
|
+
label: entityConfig.label,
|
|
100
|
+
global: entityConfig.global || false,
|
|
101
|
+
autoProvision: entityConfig.autoProvision || false,
|
|
102
|
+
required: entityConfig.required !== false
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
this.updatedAt = new Date();
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Validate integration business rules
|
|
111
|
+
*/
|
|
112
|
+
validate() {
|
|
113
|
+
const errors = [];
|
|
114
|
+
|
|
115
|
+
// Display name validation
|
|
116
|
+
if (!this.displayName || this.displayName.trim().length === 0) {
|
|
117
|
+
errors.push('Display name is required');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Description validation
|
|
121
|
+
if (this.description && this.description.length > 1000) {
|
|
122
|
+
errors.push('Description must be 1000 characters or less');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Type validation
|
|
126
|
+
const validTypes = ['api', 'webhook', 'sync', 'transform', 'custom'];
|
|
127
|
+
if (!validTypes.includes(this.type)) {
|
|
128
|
+
errors.push(`Invalid integration type: ${this.type}. Must be one of: ${validTypes.join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Entity validation
|
|
132
|
+
if (Object.keys(this.entities).length === 0) {
|
|
133
|
+
// Warning: integration with no entities is unusual but not invalid
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
isValid: errors.length === 0,
|
|
138
|
+
errors
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Convert to plain object (for persistence)
|
|
144
|
+
*/
|
|
145
|
+
toObject() {
|
|
146
|
+
return {
|
|
147
|
+
id: this.id.value,
|
|
148
|
+
name: this.name.value,
|
|
149
|
+
version: this.version.value,
|
|
150
|
+
displayName: this.displayName,
|
|
151
|
+
description: this.description,
|
|
152
|
+
type: this.type,
|
|
153
|
+
category: this.category,
|
|
154
|
+
tags: this.tags,
|
|
155
|
+
entities: this.entities,
|
|
156
|
+
apiModules: this.apiModules,
|
|
157
|
+
capabilities: this.capabilities,
|
|
158
|
+
requirements: this.requirements,
|
|
159
|
+
options: this.options,
|
|
160
|
+
createdAt: this.createdAt,
|
|
161
|
+
updatedAt: this.updatedAt
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Convert to JSON format (for integration-definition.json)
|
|
167
|
+
* Follows the integration-definition.schema.json structure
|
|
168
|
+
*/
|
|
169
|
+
toJSON() {
|
|
170
|
+
return {
|
|
171
|
+
name: this.name.value,
|
|
172
|
+
version: this.version.value,
|
|
173
|
+
options: {
|
|
174
|
+
type: this.type,
|
|
175
|
+
display: {
|
|
176
|
+
name: this.displayName,
|
|
177
|
+
description: this.description || '',
|
|
178
|
+
category: this.category,
|
|
179
|
+
tags: this.tags
|
|
180
|
+
},
|
|
181
|
+
...this.options
|
|
182
|
+
},
|
|
183
|
+
entities: this.entities,
|
|
184
|
+
capabilities: this.capabilities,
|
|
185
|
+
requirements: this.requirements
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_generateDisplayName() {
|
|
190
|
+
// Convert kebab-case to Title Case
|
|
191
|
+
return this.name.value
|
|
192
|
+
.split('-')
|
|
193
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
194
|
+
.join(' ');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {Integration};
|