@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.
Files changed (127) hide show
  1. package/frigg-cli/README.md +1 -1
  2. package/frigg-cli/__tests__/application/use-cases/AddApiModuleToIntegrationUseCase.test.js +326 -0
  3. package/frigg-cli/__tests__/application/use-cases/CreateApiModuleUseCase.test.js +337 -0
  4. package/frigg-cli/__tests__/domain/entities/ApiModule.test.js +373 -0
  5. package/frigg-cli/__tests__/domain/entities/AppDefinition.test.js +313 -0
  6. package/frigg-cli/__tests__/domain/services/IntegrationValidator.test.js +269 -0
  7. package/frigg-cli/__tests__/domain/value-objects/IntegrationName.test.js +82 -0
  8. package/frigg-cli/__tests__/infrastructure/adapters/IntegrationJsUpdater.test.js +408 -0
  9. package/frigg-cli/__tests__/infrastructure/repositories/FileSystemApiModuleRepository.test.js +583 -0
  10. package/frigg-cli/__tests__/infrastructure/repositories/FileSystemAppDefinitionRepository.test.js +314 -0
  11. package/frigg-cli/__tests__/infrastructure/repositories/FileSystemIntegrationRepository.test.js +383 -0
  12. package/frigg-cli/__tests__/unit/commands/build.test.js +1 -1
  13. package/frigg-cli/__tests__/unit/commands/doctor.test.js +0 -2
  14. package/frigg-cli/__tests__/unit/commands/init.test.js +406 -0
  15. package/frigg-cli/__tests__/unit/commands/install.test.js +23 -19
  16. package/frigg-cli/__tests__/unit/commands/provider-dispatch.test.js +383 -0
  17. package/frigg-cli/__tests__/unit/commands/repair.test.js +275 -0
  18. package/frigg-cli/__tests__/unit/dependencies.test.js +2 -2
  19. package/frigg-cli/__tests__/unit/start-command/application/RunPreflightChecksUseCase.test.js +411 -0
  20. package/frigg-cli/__tests__/unit/start-command/infrastructure/DatabaseAdapter.test.js +405 -0
  21. package/frigg-cli/__tests__/unit/start-command/infrastructure/DockerAdapter.test.js +496 -0
  22. package/frigg-cli/__tests__/unit/start-command/presentation/InteractivePromptAdapter.test.js +474 -0
  23. package/frigg-cli/__tests__/unit/utils/output.test.js +196 -0
  24. package/frigg-cli/application/use-cases/AddApiModuleToIntegrationUseCase.js +93 -0
  25. package/frigg-cli/application/use-cases/CreateApiModuleUseCase.js +93 -0
  26. package/frigg-cli/application/use-cases/CreateIntegrationUseCase.js +103 -0
  27. package/frigg-cli/build-command/index.js +123 -11
  28. package/frigg-cli/container.js +172 -0
  29. package/frigg-cli/deploy-command/index.js +83 -1
  30. package/frigg-cli/docs/OUTPUT_MIGRATION_GUIDE.md +286 -0
  31. package/frigg-cli/doctor-command/index.js +37 -16
  32. package/frigg-cli/domain/entities/ApiModule.js +272 -0
  33. package/frigg-cli/domain/entities/AppDefinition.js +227 -0
  34. package/frigg-cli/domain/entities/Integration.js +198 -0
  35. package/frigg-cli/domain/exceptions/DomainException.js +24 -0
  36. package/frigg-cli/domain/ports/IApiModuleRepository.js +53 -0
  37. package/frigg-cli/domain/ports/IAppDefinitionRepository.js +43 -0
  38. package/frigg-cli/domain/ports/IIntegrationRepository.js +61 -0
  39. package/frigg-cli/domain/services/IntegrationValidator.js +185 -0
  40. package/frigg-cli/domain/value-objects/IntegrationId.js +42 -0
  41. package/frigg-cli/domain/value-objects/IntegrationName.js +60 -0
  42. package/frigg-cli/domain/value-objects/SemanticVersion.js +70 -0
  43. package/frigg-cli/generate-iam-command.js +21 -1
  44. package/frigg-cli/index.js +21 -6
  45. package/frigg-cli/index.test.js +7 -2
  46. package/frigg-cli/infrastructure/UnitOfWork.js +46 -0
  47. package/frigg-cli/infrastructure/adapters/BackendJsUpdater.js +197 -0
  48. package/frigg-cli/infrastructure/adapters/FileSystemAdapter.js +224 -0
  49. package/frigg-cli/infrastructure/adapters/IntegrationJsUpdater.js +249 -0
  50. package/frigg-cli/infrastructure/adapters/SchemaValidator.js +92 -0
  51. package/frigg-cli/infrastructure/repositories/FileSystemApiModuleRepository.js +373 -0
  52. package/frigg-cli/infrastructure/repositories/FileSystemAppDefinitionRepository.js +116 -0
  53. package/frigg-cli/infrastructure/repositories/FileSystemIntegrationRepository.js +277 -0
  54. package/frigg-cli/init-command/backend-first-handler.js +124 -42
  55. package/frigg-cli/init-command/index.js +2 -1
  56. package/frigg-cli/init-command/template-handler.js +13 -3
  57. package/frigg-cli/install-command/backend-js.js +3 -3
  58. package/frigg-cli/install-command/environment-variables.js +16 -19
  59. package/frigg-cli/install-command/environment-variables.test.js +12 -13
  60. package/frigg-cli/install-command/index.js +14 -9
  61. package/frigg-cli/install-command/integration-file.js +3 -3
  62. package/frigg-cli/install-command/validate-package.js +5 -9
  63. package/frigg-cli/jest.config.js +4 -1
  64. package/frigg-cli/package-lock.json +16226 -0
  65. package/frigg-cli/repair-command/index.js +121 -128
  66. package/frigg-cli/start-command/application/RunPreflightChecksUseCase.js +376 -0
  67. package/frigg-cli/start-command/index.js +324 -2
  68. package/frigg-cli/start-command/infrastructure/DatabaseAdapter.js +591 -0
  69. package/frigg-cli/start-command/infrastructure/DockerAdapter.js +306 -0
  70. package/frigg-cli/start-command/presentation/InteractivePromptAdapter.js +329 -0
  71. package/frigg-cli/templates/backend/.env.example +62 -0
  72. package/frigg-cli/templates/backend/.eslintrc.json +12 -0
  73. package/frigg-cli/templates/backend/.prettierrc +6 -0
  74. package/frigg-cli/templates/backend/docker-compose.yml +22 -0
  75. package/frigg-cli/templates/backend/index.js +96 -0
  76. package/frigg-cli/templates/backend/infrastructure.js +12 -0
  77. package/frigg-cli/templates/backend/jest.config.js +17 -0
  78. package/frigg-cli/templates/backend/package.json +50 -0
  79. package/frigg-cli/templates/backend/src/api-modules/.gitkeep +10 -0
  80. package/frigg-cli/templates/backend/src/base/.gitkeep +7 -0
  81. package/frigg-cli/templates/backend/src/integrations/.gitkeep +10 -0
  82. package/frigg-cli/templates/backend/src/integrations/ExampleIntegration.js +65 -0
  83. package/frigg-cli/templates/backend/src/utils/.gitkeep +7 -0
  84. package/frigg-cli/templates/backend/test/setup.js +30 -0
  85. package/frigg-cli/templates/backend/ui-extensions/.gitkeep +0 -0
  86. package/frigg-cli/templates/backend/ui-extensions/README.md +77 -0
  87. package/frigg-cli/ui-command/index.js +58 -36
  88. package/frigg-cli/utils/__tests__/provider-helper.test.js +55 -0
  89. package/frigg-cli/utils/__tests__/repo-detection.test.js +436 -0
  90. package/frigg-cli/utils/output.js +382 -0
  91. package/frigg-cli/utils/provider-helper.js +75 -0
  92. package/frigg-cli/utils/repo-detection.js +85 -37
  93. package/frigg-cli/validate-command/__tests__/adapters/validate-command.test.js +205 -0
  94. package/frigg-cli/validate-command/__tests__/application/validate-app-use-case.test.js +104 -0
  95. package/frigg-cli/validate-command/__tests__/domain/fix-suggestion.test.js +153 -0
  96. package/frigg-cli/validate-command/__tests__/domain/validation-error.test.js +162 -0
  97. package/frigg-cli/validate-command/__tests__/domain/validation-result.test.js +152 -0
  98. package/frigg-cli/validate-command/__tests__/infrastructure/api-module-validator.test.js +332 -0
  99. package/frigg-cli/validate-command/__tests__/infrastructure/app-definition-validator.test.js +191 -0
  100. package/frigg-cli/validate-command/__tests__/infrastructure/integration-class-validator.test.js +146 -0
  101. package/frigg-cli/validate-command/__tests__/infrastructure/template-validation.test.js +155 -0
  102. package/frigg-cli/validate-command/adapters/cli/validate-command.js +199 -0
  103. package/frigg-cli/validate-command/application/use-cases/validate-app-use-case.js +35 -0
  104. package/frigg-cli/validate-command/domain/entities/validation-result.js +74 -0
  105. package/frigg-cli/validate-command/domain/value-objects/fix-suggestion.js +74 -0
  106. package/frigg-cli/validate-command/domain/value-objects/validation-error.js +68 -0
  107. package/frigg-cli/validate-command/infrastructure/validators/api-module-validator.js +181 -0
  108. package/frigg-cli/validate-command/infrastructure/validators/app-definition-validator.js +128 -0
  109. package/frigg-cli/validate-command/infrastructure/validators/integration-class-validator.js +113 -0
  110. package/infrastructure/create-frigg-infrastructure.js +93 -0
  111. package/infrastructure/docs/iam-policy-templates.md +1 -1
  112. package/infrastructure/domains/admin-scripts/admin-script-builder.js +200 -0
  113. package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +499 -0
  114. package/infrastructure/domains/admin-scripts/index.js +5 -0
  115. package/infrastructure/domains/networking/vpc-builder.test.js +2 -4
  116. package/infrastructure/domains/networking/vpc-resolver.test.js +1 -1
  117. package/infrastructure/domains/shared/resource-discovery.js +5 -5
  118. package/infrastructure/domains/shared/types/app-definition.js +21 -0
  119. package/infrastructure/domains/shared/types/discovery-result.test.js +1 -1
  120. package/infrastructure/domains/shared/utilities/base-definition-factory.js +10 -1
  121. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +2 -2
  122. package/infrastructure/infrastructure-composer.js +2 -0
  123. package/infrastructure/infrastructure-composer.test.js +2 -2
  124. package/infrastructure/jest.config.js +16 -0
  125. package/management-ui/README.md +245 -109
  126. package/package.json +8 -7
  127. 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};