@friggframework/devtools 2.0.0--canary.517.179491e.0 → 2.0.0--canary.522.893db5d.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 (115) 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/doctor.test.js +0 -2
  13. package/frigg-cli/__tests__/unit/commands/init.test.js +406 -0
  14. package/frigg-cli/__tests__/unit/commands/install.test.js +23 -19
  15. package/frigg-cli/__tests__/unit/commands/repair.test.js +275 -0
  16. package/frigg-cli/__tests__/unit/dependencies.test.js +2 -2
  17. package/frigg-cli/__tests__/unit/start-command/application/RunPreflightChecksUseCase.test.js +411 -0
  18. package/frigg-cli/__tests__/unit/start-command/infrastructure/DatabaseAdapter.test.js +405 -0
  19. package/frigg-cli/__tests__/unit/start-command/infrastructure/DockerAdapter.test.js +496 -0
  20. package/frigg-cli/__tests__/unit/start-command/presentation/InteractivePromptAdapter.test.js +474 -0
  21. package/frigg-cli/__tests__/unit/utils/output.test.js +196 -0
  22. package/frigg-cli/application/use-cases/AddApiModuleToIntegrationUseCase.js +93 -0
  23. package/frigg-cli/application/use-cases/CreateApiModuleUseCase.js +93 -0
  24. package/frigg-cli/application/use-cases/CreateIntegrationUseCase.js +103 -0
  25. package/frigg-cli/container.js +172 -0
  26. package/frigg-cli/docs/OUTPUT_MIGRATION_GUIDE.md +286 -0
  27. package/frigg-cli/doctor-command/index.js +17 -16
  28. package/frigg-cli/domain/entities/ApiModule.js +272 -0
  29. package/frigg-cli/domain/entities/AppDefinition.js +227 -0
  30. package/frigg-cli/domain/entities/Integration.js +198 -0
  31. package/frigg-cli/domain/exceptions/DomainException.js +24 -0
  32. package/frigg-cli/domain/ports/IApiModuleRepository.js +53 -0
  33. package/frigg-cli/domain/ports/IAppDefinitionRepository.js +43 -0
  34. package/frigg-cli/domain/ports/IIntegrationRepository.js +61 -0
  35. package/frigg-cli/domain/services/IntegrationValidator.js +185 -0
  36. package/frigg-cli/domain/value-objects/IntegrationId.js +42 -0
  37. package/frigg-cli/domain/value-objects/IntegrationName.js +60 -0
  38. package/frigg-cli/domain/value-objects/SemanticVersion.js +70 -0
  39. package/frigg-cli/index.js +21 -6
  40. package/frigg-cli/index.test.js +7 -2
  41. package/frigg-cli/infrastructure/UnitOfWork.js +46 -0
  42. package/frigg-cli/infrastructure/adapters/BackendJsUpdater.js +197 -0
  43. package/frigg-cli/infrastructure/adapters/FileSystemAdapter.js +224 -0
  44. package/frigg-cli/infrastructure/adapters/IntegrationJsUpdater.js +249 -0
  45. package/frigg-cli/infrastructure/adapters/SchemaValidator.js +92 -0
  46. package/frigg-cli/infrastructure/repositories/FileSystemApiModuleRepository.js +373 -0
  47. package/frigg-cli/infrastructure/repositories/FileSystemAppDefinitionRepository.js +116 -0
  48. package/frigg-cli/infrastructure/repositories/FileSystemIntegrationRepository.js +277 -0
  49. package/frigg-cli/init-command/backend-first-handler.js +124 -42
  50. package/frigg-cli/init-command/index.js +2 -1
  51. package/frigg-cli/init-command/template-handler.js +13 -3
  52. package/frigg-cli/install-command/backend-js.js +3 -3
  53. package/frigg-cli/install-command/environment-variables.js +16 -19
  54. package/frigg-cli/install-command/environment-variables.test.js +12 -13
  55. package/frigg-cli/install-command/index.js +14 -9
  56. package/frigg-cli/install-command/integration-file.js +3 -3
  57. package/frigg-cli/install-command/validate-package.js +5 -9
  58. package/frigg-cli/jest.config.js +4 -1
  59. package/frigg-cli/package-lock.json +16226 -0
  60. package/frigg-cli/repair-command/index.js +101 -128
  61. package/frigg-cli/start-command/application/RunPreflightChecksUseCase.js +376 -0
  62. package/frigg-cli/start-command/index.js +246 -2
  63. package/frigg-cli/start-command/infrastructure/DatabaseAdapter.js +591 -0
  64. package/frigg-cli/start-command/infrastructure/DockerAdapter.js +306 -0
  65. package/frigg-cli/start-command/presentation/InteractivePromptAdapter.js +329 -0
  66. package/frigg-cli/templates/backend/.env.example +62 -0
  67. package/frigg-cli/templates/backend/.eslintrc.json +12 -0
  68. package/frigg-cli/templates/backend/.prettierrc +6 -0
  69. package/frigg-cli/templates/backend/docker-compose.yml +22 -0
  70. package/frigg-cli/templates/backend/index.js +96 -0
  71. package/frigg-cli/templates/backend/infrastructure.js +12 -0
  72. package/frigg-cli/templates/backend/jest.config.js +17 -0
  73. package/frigg-cli/templates/backend/package.json +50 -0
  74. package/frigg-cli/templates/backend/src/api-modules/.gitkeep +10 -0
  75. package/frigg-cli/templates/backend/src/base/.gitkeep +7 -0
  76. package/frigg-cli/templates/backend/src/integrations/.gitkeep +10 -0
  77. package/frigg-cli/templates/backend/src/integrations/ExampleIntegration.js +65 -0
  78. package/frigg-cli/templates/backend/src/utils/.gitkeep +7 -0
  79. package/frigg-cli/templates/backend/test/setup.js +30 -0
  80. package/frigg-cli/templates/backend/ui-extensions/.gitkeep +0 -0
  81. package/frigg-cli/templates/backend/ui-extensions/README.md +77 -0
  82. package/frigg-cli/ui-command/index.js +58 -36
  83. package/frigg-cli/utils/__tests__/repo-detection.test.js +436 -0
  84. package/frigg-cli/utils/output.js +382 -0
  85. package/frigg-cli/utils/repo-detection.js +85 -37
  86. package/frigg-cli/validate-command/__tests__/adapters/validate-command.test.js +205 -0
  87. package/frigg-cli/validate-command/__tests__/application/validate-app-use-case.test.js +104 -0
  88. package/frigg-cli/validate-command/__tests__/domain/fix-suggestion.test.js +153 -0
  89. package/frigg-cli/validate-command/__tests__/domain/validation-error.test.js +162 -0
  90. package/frigg-cli/validate-command/__tests__/domain/validation-result.test.js +152 -0
  91. package/frigg-cli/validate-command/__tests__/infrastructure/api-module-validator.test.js +332 -0
  92. package/frigg-cli/validate-command/__tests__/infrastructure/app-definition-validator.test.js +191 -0
  93. package/frigg-cli/validate-command/__tests__/infrastructure/integration-class-validator.test.js +146 -0
  94. package/frigg-cli/validate-command/__tests__/infrastructure/template-validation.test.js +155 -0
  95. package/frigg-cli/validate-command/adapters/cli/validate-command.js +199 -0
  96. package/frigg-cli/validate-command/application/use-cases/validate-app-use-case.js +35 -0
  97. package/frigg-cli/validate-command/domain/entities/validation-result.js +74 -0
  98. package/frigg-cli/validate-command/domain/value-objects/fix-suggestion.js +74 -0
  99. package/frigg-cli/validate-command/domain/value-objects/validation-error.js +68 -0
  100. package/frigg-cli/validate-command/infrastructure/validators/api-module-validator.js +181 -0
  101. package/frigg-cli/validate-command/infrastructure/validators/app-definition-validator.js +128 -0
  102. package/frigg-cli/validate-command/infrastructure/validators/integration-class-validator.js +113 -0
  103. package/infrastructure/docs/iam-policy-templates.md +1 -1
  104. package/infrastructure/domains/networking/vpc-builder.test.js +2 -4
  105. package/infrastructure/domains/networking/vpc-resolver.test.js +1 -1
  106. package/infrastructure/domains/shared/cloudformation-discovery.test.js +4 -7
  107. package/infrastructure/domains/shared/resource-discovery.js +5 -5
  108. package/infrastructure/domains/shared/types/discovery-result.test.js +1 -1
  109. package/infrastructure/domains/shared/utilities/base-definition-factory.js +10 -1
  110. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +2 -2
  111. package/infrastructure/infrastructure-composer.test.js +2 -2
  112. package/infrastructure/jest.config.js +16 -0
  113. package/management-ui/README.md +245 -109
  114. package/package.json +8 -7
  115. package/frigg-cli/install-command/logger.js +0 -12
@@ -0,0 +1,68 @@
1
+ const VALID_SEVERITIES = ['error', 'warning', 'info'];
2
+
3
+ class ValidationError {
4
+ constructor({ path, message, severity, code, fix }) {
5
+ if (!path) {
6
+ throw new Error('path is required');
7
+ }
8
+ if (!message) {
9
+ throw new Error('message is required');
10
+ }
11
+ if (severity && !VALID_SEVERITIES.includes(severity)) {
12
+ throw new Error(`Invalid severity: ${severity}. Must be one of: ${VALID_SEVERITIES.join(', ')}`);
13
+ }
14
+
15
+ this.path = path;
16
+ this.message = message;
17
+ this.severity = severity || 'error';
18
+ this.code = code || null;
19
+ this.fix = fix || null;
20
+ }
21
+
22
+ static create(props) {
23
+ return new ValidationError(props);
24
+ }
25
+
26
+ getPathSegments() {
27
+ return this.path
28
+ .replace(/\[(\d+)\]/g, '.$1')
29
+ .split('.')
30
+ .filter(Boolean);
31
+ }
32
+
33
+ getRootPath() {
34
+ return this.getPathSegments()[0];
35
+ }
36
+
37
+ isError() {
38
+ return this.severity === 'error';
39
+ }
40
+
41
+ isWarning() {
42
+ return this.severity === 'warning';
43
+ }
44
+
45
+ isInfo() {
46
+ return this.severity === 'info';
47
+ }
48
+
49
+ hasFix() {
50
+ return this.fix !== null;
51
+ }
52
+
53
+ equals(other) {
54
+ return this.path === other.path && this.message === other.message;
55
+ }
56
+
57
+ toJSON() {
58
+ return {
59
+ path: this.path,
60
+ message: this.message,
61
+ severity: this.severity,
62
+ code: this.code,
63
+ fix: this.fix ? this.fix.toJSON() : null
64
+ };
65
+ }
66
+ }
67
+
68
+ module.exports = { ValidationError };
@@ -0,0 +1,181 @@
1
+ const { validateApiModuleDefinition } = require('@friggframework/schemas');
2
+ const { ValidationResult } = require('../../domain/entities/validation-result');
3
+ const { ValidationError } = require('../../domain/value-objects/validation-error');
4
+
5
+ const REQUIRED_MODULE_METHODS = ['getToken', 'getEntityDetails', 'getCredentialDetails'];
6
+
7
+ class ApiModuleValidator {
8
+ /**
9
+ * Validate API module definitions within an integration
10
+ * @param {object} integrationDefinition - The integration's Definition object
11
+ * @param {number} integrationIndex - Index of the integration in the app
12
+ * @returns {ValidationResult}
13
+ */
14
+ validate(integrationDefinition, integrationIndex) {
15
+ const result = ValidationResult.create();
16
+ const prefix = `integrations[${integrationIndex}].Definition.modules`;
17
+
18
+ if (!integrationDefinition.modules || typeof integrationDefinition.modules !== 'object') {
19
+ return result;
20
+ }
21
+
22
+ Object.entries(integrationDefinition.modules).forEach(([moduleName, moduleConfig]) => {
23
+ const modulePath = `${prefix}.${moduleName}`;
24
+
25
+ if (!moduleConfig.definition) {
26
+ result.addError(ValidationError.create({
27
+ path: `${modulePath}.definition`,
28
+ message: `Module '${moduleName}' must have a definition property`,
29
+ severity: 'error',
30
+ code: 'MISSING_DEFINITION'
31
+ }));
32
+ return;
33
+ }
34
+
35
+ this._validateModuleDefinitionWithSchema(moduleConfig.definition, modulePath, moduleName, result);
36
+ this._validateRequiredMethods(moduleConfig.definition, modulePath, moduleName, result);
37
+ this._validateApiPropertiesToPersist(moduleConfig.definition, modulePath, moduleName, result);
38
+ });
39
+
40
+ return result;
41
+ }
42
+
43
+ _validateModuleDefinitionWithSchema(definition, modulePath, moduleName, result) {
44
+ // Sanitize the definition before JSON Schema validation
45
+ // API module definitions contain functions and classes that JSON Schema can't validate
46
+ const sanitizedDefinition = this._sanitizeForSchemaValidation(definition);
47
+ const schemaResult = validateApiModuleDefinition(sanitizedDefinition);
48
+
49
+ if (!schemaResult.valid && schemaResult.errors) {
50
+ schemaResult.errors.forEach(error => {
51
+ const path = error.instancePath
52
+ ? `${modulePath}.definition${error.instancePath.replace(/\//g, '.')}`
53
+ : `${modulePath}.definition`;
54
+
55
+ result.addError(ValidationError.create({
56
+ path,
57
+ message: this._formatSchemaErrorMessage(error),
58
+ severity: 'error',
59
+ code: error.keyword?.toUpperCase() || 'SCHEMA_ERROR'
60
+ }));
61
+ });
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Create a copy of the module definition safe for JSON Schema validation.
67
+ * Converts functions to descriptors but preserves all properties so that
68
+ * unknown properties can be properly rejected by the schema.
69
+ */
70
+ _sanitizeForSchemaValidation(definition) {
71
+ if (!definition) return definition;
72
+
73
+ const sanitized = {};
74
+
75
+ // Copy ALL properties, converting functions/classes to descriptors
76
+ // This allows JSON Schema to properly reject unknown properties
77
+ for (const key of Object.keys(definition)) {
78
+ sanitized[key] = this._sanitizeValue(definition[key]);
79
+ }
80
+
81
+ return sanitized;
82
+ }
83
+
84
+ /**
85
+ * Recursively sanitize a value for JSON Schema validation.
86
+ * Functions become {type: "function"} descriptors.
87
+ * Classes become {type: "object"} descriptors.
88
+ */
89
+ _sanitizeValue(value) {
90
+ if (value === null || value === undefined) {
91
+ return value;
92
+ }
93
+
94
+ // Convert functions to descriptors
95
+ if (typeof value === 'function') {
96
+ return { type: 'function', name: value.name || 'anonymous' };
97
+ }
98
+
99
+ // Handle arrays
100
+ if (Array.isArray(value)) {
101
+ return value.map(item => this._sanitizeValue(item));
102
+ }
103
+
104
+ // Handle objects (but not class instances with constructors other than Object)
105
+ if (typeof value === 'object') {
106
+ // Check if it's a class instance (not a plain object)
107
+ if (value.constructor && value.constructor.name !== 'Object') {
108
+ return { type: 'object', className: value.constructor.name };
109
+ }
110
+
111
+ // Recursively sanitize plain objects
112
+ const sanitizedObj = {};
113
+ for (const [key, val] of Object.entries(value)) {
114
+ sanitizedObj[key] = this._sanitizeValue(val);
115
+ }
116
+ return sanitizedObj;
117
+ }
118
+
119
+ // Primitives pass through unchanged
120
+ return value;
121
+ }
122
+
123
+ _formatSchemaErrorMessage(error) {
124
+ let message = error.message;
125
+ if (error.params?.allowedValues) {
126
+ message += ` (allowed: ${error.params.allowedValues.join(', ')})`;
127
+ }
128
+ if (error.params?.additionalProperty) {
129
+ message += `: ${error.params.additionalProperty}`;
130
+ }
131
+ if (error.params?.missingProperty) {
132
+ message = `must have required property '${error.params.missingProperty}'`;
133
+ }
134
+ return message;
135
+ }
136
+
137
+ _validateRequiredMethods(definition, modulePath, moduleName, result) {
138
+ if (!definition.requiredAuthMethods) {
139
+ return;
140
+ }
141
+
142
+ REQUIRED_MODULE_METHODS.forEach(method => {
143
+ if (!definition.requiredAuthMethods[method]) {
144
+ result.addError(ValidationError.create({
145
+ path: `${modulePath}.definition.requiredAuthMethods.${method}`,
146
+ message: `Module '${moduleName}' should implement ${method} method`,
147
+ severity: 'warning',
148
+ code: 'MISSING_METHOD'
149
+ }));
150
+ }
151
+ });
152
+ }
153
+
154
+ _validateApiPropertiesToPersist(definition, modulePath, moduleName, result) {
155
+ const props = definition.requiredAuthMethods?.apiPropertiesToPersist;
156
+
157
+ if (!props) {
158
+ return;
159
+ }
160
+
161
+ if (!props.credential || !Array.isArray(props.credential) || props.credential.length === 0) {
162
+ result.addError(ValidationError.create({
163
+ path: `${modulePath}.definition.requiredAuthMethods.apiPropertiesToPersist.credential`,
164
+ message: `Module '${moduleName}' should specify credential properties to persist`,
165
+ severity: 'warning',
166
+ code: 'MISSING_CREDENTIAL_PROPS'
167
+ }));
168
+ }
169
+
170
+ if (!props.entity || !Array.isArray(props.entity) || props.entity.length === 0) {
171
+ result.addError(ValidationError.create({
172
+ path: `${modulePath}.definition.requiredAuthMethods.apiPropertiesToPersist.entity`,
173
+ message: `Module '${moduleName}' should specify entity properties to persist`,
174
+ severity: 'warning',
175
+ code: 'MISSING_ENTITY_PROPS'
176
+ }));
177
+ }
178
+ }
179
+ }
180
+
181
+ module.exports = { ApiModuleValidator };
@@ -0,0 +1,128 @@
1
+ const { validateAppDefinition } = require('@friggframework/schemas');
2
+ const { ValidationResult } = require('../../domain/entities/validation-result');
3
+ const { ValidationError } = require('../../domain/value-objects/validation-error');
4
+
5
+ class AppDefinitionValidator {
6
+ validate(definition) {
7
+ const result = ValidationResult.create();
8
+
9
+ // Create a sanitized copy for JSON Schema validation
10
+ // Integrations contain classes/functions which JSON Schema can't validate
11
+ // IntegrationClassValidator handles those separately
12
+ const sanitizedDefinition = this._sanitizeForSchemaValidation(definition);
13
+ const schemaResult = validateAppDefinition(sanitizedDefinition);
14
+
15
+ if (!schemaResult.valid && schemaResult.errors) {
16
+ schemaResult.errors.forEach(error => {
17
+ result.addError(ValidationError.create({
18
+ path: this._convertPath(error.instancePath) || 'root',
19
+ message: this._formatErrorMessage(error),
20
+ severity: 'error',
21
+ code: error.keyword?.toUpperCase() || 'SCHEMA_ERROR'
22
+ }));
23
+ });
24
+ }
25
+
26
+ this._validateIntegrationDuplicates(definition, result);
27
+ this._validateIntegrationDefinitions(definition, result);
28
+
29
+ return result;
30
+ }
31
+
32
+ /**
33
+ * Create a copy of the definition safe for JSON Schema validation.
34
+ * Replaces integration classes with stub objects since JSON Schema
35
+ * cannot validate JavaScript classes/functions.
36
+ */
37
+ _sanitizeForSchemaValidation(definition) {
38
+ if (!definition) return definition;
39
+
40
+ const sanitized = { ...definition };
41
+
42
+ // Replace integration classes with stub objects
43
+ // The actual class validation is handled by IntegrationClassValidator
44
+ if (Array.isArray(definition.integrations)) {
45
+ sanitized.integrations = definition.integrations.map(integration => {
46
+ if (typeof integration === 'function') {
47
+ // Return a stub object representing the class
48
+ return { _isClass: true, name: integration.name };
49
+ }
50
+ return integration;
51
+ });
52
+ }
53
+
54
+ return sanitized;
55
+ }
56
+
57
+ _convertPath(jsonPointerPath) {
58
+ if (!jsonPointerPath) return '';
59
+ return jsonPointerPath
60
+ .replace(/^\//, '')
61
+ .replace(/\/(\d+)/g, '[$1]')
62
+ .replace(/\//g, '.');
63
+ }
64
+
65
+ _formatErrorMessage(error) {
66
+ let message = error.message;
67
+ if (error.params?.allowedValues) {
68
+ message += ` (allowed: ${error.params.allowedValues.join(', ')})`;
69
+ }
70
+ if (error.params?.additionalProperty) {
71
+ message += `: ${error.params.additionalProperty}`;
72
+ }
73
+ if (error.params?.missingProperty) {
74
+ message = `must have required property '${error.params.missingProperty}'`;
75
+ }
76
+ return message;
77
+ }
78
+
79
+ _validateIntegrationDefinitions(definition, result) {
80
+ if (!definition.integrations || !Array.isArray(definition.integrations)) {
81
+ return;
82
+ }
83
+
84
+ definition.integrations.forEach((integration, index) => {
85
+ if (typeof integration === 'function') {
86
+ if (!integration.Definition) {
87
+ result.addError(ValidationError.create({
88
+ path: `integrations[${index}]`,
89
+ message: 'Integration class must have a static Definition property',
90
+ severity: 'error',
91
+ code: 'MISSING_DEFINITION'
92
+ }));
93
+ } else if (!integration.Definition.name) {
94
+ result.addError(ValidationError.create({
95
+ path: `integrations[${index}].Definition.name`,
96
+ message: 'Integration Definition must have a name property',
97
+ severity: 'error',
98
+ code: 'REQUIRED_FIELD'
99
+ }));
100
+ }
101
+ }
102
+ });
103
+ }
104
+
105
+ _validateIntegrationDuplicates(definition, result) {
106
+ if (!definition.integrations || !Array.isArray(definition.integrations)) {
107
+ return;
108
+ }
109
+
110
+ const names = [];
111
+ definition.integrations.forEach((integration, index) => {
112
+ const name = integration.Definition?.name;
113
+ if (name) {
114
+ if (names.includes(name)) {
115
+ result.addError(ValidationError.create({
116
+ path: `integrations[${index}].Definition.name`,
117
+ message: `duplicate integration name: ${name}`,
118
+ severity: 'warning',
119
+ code: 'DUPLICATE_NAME'
120
+ }));
121
+ }
122
+ names.push(name);
123
+ }
124
+ });
125
+ }
126
+ }
127
+
128
+ module.exports = { AppDefinitionValidator };
@@ -0,0 +1,113 @@
1
+ const { validateIntegrationDefinition } = require('@friggframework/schemas');
2
+ const { ValidationResult } = require('../../domain/entities/validation-result');
3
+ const { ValidationError } = require('../../domain/value-objects/validation-error');
4
+ const { FixSuggestion } = require('../../domain/value-objects/fix-suggestion');
5
+
6
+ const LIFECYCLE_METHODS = ['onCreate', 'getConfigOptions', 'testAuth'];
7
+
8
+ class IntegrationClassValidator {
9
+ validate(integrationClass, index) {
10
+ const result = ValidationResult.create();
11
+ const prefix = `integrations[${index}]`;
12
+
13
+ if (typeof integrationClass !== 'function') {
14
+ result.addError(ValidationError.create({
15
+ path: prefix,
16
+ message: 'Integration must be a class (function)',
17
+ severity: 'error',
18
+ code: 'INVALID_TYPE'
19
+ }));
20
+ return result;
21
+ }
22
+
23
+ if (!integrationClass.Definition) {
24
+ result.addError(ValidationError.create({
25
+ path: `${prefix}.Definition`,
26
+ message: 'Integration class must have a static Definition property',
27
+ severity: 'error',
28
+ code: 'MISSING_DEFINITION',
29
+ fix: FixSuggestion.create({
30
+ action: 'add',
31
+ description: 'Add static Definition property to integration class',
32
+ codeSnippet: `static Definition = {\n name: 'my-integration',\n version: '1.0.0',\n modules: {}\n};`
33
+ })
34
+ }));
35
+ return result;
36
+ }
37
+
38
+ this._validateDefinitionWithSchema(integrationClass.Definition, prefix, result);
39
+ this._validateModuleNames(integrationClass.Definition, prefix, result);
40
+ this._validateLifecycleMethods(integrationClass, prefix, result);
41
+
42
+ return result;
43
+ }
44
+
45
+ _validateDefinitionWithSchema(definition, prefix, result) {
46
+ const schemaResult = validateIntegrationDefinition(definition);
47
+
48
+ if (!schemaResult.valid && schemaResult.errors) {
49
+ schemaResult.errors.forEach(error => {
50
+ const path = error.instancePath
51
+ ? `${prefix}.Definition${error.instancePath.replace(/\//g, '.')}`
52
+ : `${prefix}.Definition`;
53
+
54
+ result.addError(ValidationError.create({
55
+ path,
56
+ message: this._formatSchemaErrorMessage(error),
57
+ severity: 'error',
58
+ code: error.keyword?.toUpperCase() || 'SCHEMA_ERROR'
59
+ }));
60
+ });
61
+ }
62
+ }
63
+
64
+ _formatSchemaErrorMessage(error) {
65
+ let message = error.message;
66
+ if (error.params?.allowedValues) {
67
+ message += ` (allowed: ${error.params.allowedValues.join(', ')})`;
68
+ }
69
+ if (error.params?.additionalProperty) {
70
+ message += `: ${error.params.additionalProperty}`;
71
+ }
72
+ if (error.params?.missingProperty) {
73
+ message = `must have required property '${error.params.missingProperty}'`;
74
+ }
75
+ return message;
76
+ }
77
+
78
+ _validateModuleNames(definition, prefix, result) {
79
+ if (!definition.modules) {
80
+ return;
81
+ }
82
+
83
+ Object.entries(definition.modules).forEach(([moduleName, moduleConfig]) => {
84
+ // Check for moduleName (the correct property per API module schema and core Module class)
85
+ // Note: getName() method returns definition.moduleName, not definition.name
86
+ if (moduleConfig.definition && !moduleConfig.definition.moduleName) {
87
+ result.addError(ValidationError.create({
88
+ path: `${prefix}.Definition.modules.${moduleName}.definition.moduleName`,
89
+ message: `Module ${moduleName} definition should have a moduleName property`,
90
+ severity: 'warning',
91
+ code: 'MISSING_MODULE_NAME'
92
+ }));
93
+ }
94
+ });
95
+ }
96
+
97
+ _validateLifecycleMethods(integrationClass, prefix, result) {
98
+ const proto = integrationClass.prototype;
99
+
100
+ LIFECYCLE_METHODS.forEach(method => {
101
+ if (typeof proto[method] !== 'function') {
102
+ result.addError(ValidationError.create({
103
+ path: `${prefix}.${method}`,
104
+ message: `Integration should implement ${method}() method`,
105
+ severity: 'warning',
106
+ code: 'MISSING_METHOD'
107
+ }));
108
+ }
109
+ });
110
+ }
111
+ }
112
+
113
+ module.exports = { IntegrationClassValidator };
@@ -160,7 +160,7 @@ Consider separate policies for different environments:
160
160
  ### Validation
161
161
  Test your policy by deploying a simple Frigg app:
162
162
  ```bash
163
- npx create-frigg-app test-deployment
163
+ frigg init test-deployment
164
164
  cd test-deployment
165
165
  frigg deploy
166
166
  ```
@@ -1500,10 +1500,9 @@ describe('VpcBuilder', () => {
1500
1500
  }
1501
1501
  };
1502
1502
 
1503
- // Discovery results matching ACTUAL Frontify production stack
1504
1503
  const discoveredResources = {
1505
1504
  fromCloudFormationStack: true,
1506
- stackName: 'create-frigg-app-production',
1505
+ stackName: 'frigg-app-production',
1507
1506
  existingLogicalIds: [
1508
1507
  'FriggLambdaRouteTable',
1509
1508
  'FriggNATRoute', // OLD naming
@@ -1566,10 +1565,9 @@ describe('VpcBuilder', () => {
1566
1565
  });
1567
1566
 
1568
1567
  it('should convert OLD logical IDs to structured discovery stackManaged array', () => {
1569
- // TDD test: Verify that VPCEndpointS3 in existingLogicalIds gets added to stackManaged
1570
1568
  const flatDiscovery = {
1571
1569
  fromCloudFormationStack: true,
1572
- stackName: 'create-frigg-app-production',
1570
+ stackName: 'frigg-app-production',
1573
1571
  existingLogicalIds: [
1574
1572
  'VPCEndpointS3', // OLD naming
1575
1573
  'VPCEndpointDynamoDB', // OLD naming
@@ -746,7 +746,7 @@ describe('VpcResourceResolver', () => {
746
746
  ],
747
747
  external: [],
748
748
  fromCloudFormation: true,
749
- stackName: 'create-frigg-app-production'
749
+ stackName: 'frigg-app-production'
750
750
  };
751
751
 
752
752
  const decisions = resolver.resolveAll(appDefinition, discovery);
@@ -589,10 +589,8 @@ describe('CloudFormationDiscovery', () => {
589
589
 
590
590
  describe('External VPC with routing infrastructure pattern', () => {
591
591
  it('should discover routing resources when VPC is external', async () => {
592
- // This tests the external VPC pattern: external VPC/subnets/KMS,
593
- // but stack creates routing infrastructure (route table, NAT route, VPC endpoints)
594
592
  const mockStack = {
595
- StackName: 'create-frigg-app-production',
593
+ StackName: 'frigg-app-production',
596
594
  Outputs: [],
597
595
  };
598
596
 
@@ -638,7 +636,7 @@ describe('CloudFormationDiscovery', () => {
638
636
  mockProvider.describeStack.mockResolvedValue(mockStack);
639
637
  mockProvider.listStackResources.mockResolvedValue(mockResources);
640
638
 
641
- const result = await cfDiscovery.discoverFromStack('create-frigg-app-production');
639
+ const result = await cfDiscovery.discoverFromStack('frigg-app-production');
642
640
 
643
641
  // Verify routing infrastructure was discovered
644
642
  expect(result.routeTableId).toBe('rtb-0b83aca77ccde20a6');
@@ -807,9 +805,8 @@ describe('CloudFormationDiscovery', () => {
807
805
 
808
806
  describe('existingLogicalIds tracking', () => {
809
807
  it('should track OLD VPC endpoint logical IDs (VPCEndpointS3 pattern) for backwards compatibility', async () => {
810
- // CRITICAL: Frontify production uses OLD naming convention
811
808
  const mockStack = {
812
- StackName: 'create-frigg-app-production',
809
+ StackName: 'frigg-app-production',
813
810
  Outputs: []
814
811
  };
815
812
 
@@ -825,7 +822,7 @@ describe('CloudFormationDiscovery', () => {
825
822
  mockProvider.describeStack.mockResolvedValue(mockStack);
826
823
  mockProvider.listStackResources.mockResolvedValue(mockResources);
827
824
 
828
- const result = await cfDiscovery.discoverFromStack('create-frigg-app-production');
825
+ const result = await cfDiscovery.discoverFromStack('frigg-app-production');
829
826
 
830
827
  // CRITICAL: existingLogicalIds MUST contain old VPC endpoint names
831
828
  expect(result.existingLogicalIds).toBeDefined();
@@ -88,8 +88,8 @@ async function gatherDiscoveredResources(appDefinition) {
88
88
 
89
89
  // Build discovery configuration
90
90
  const stage = process.env.SLS_STAGE || 'dev';
91
- const stackName = `${appDefinition.name || 'create-frigg-app'}-${stage}`;
92
- const serviceName = appDefinition.name || 'create-frigg-app';
91
+ const stackName = `${appDefinition.name || 'frigg-app'}-${stage}`;
92
+ const serviceName = appDefinition.name || 'frigg-app';
93
93
 
94
94
  // Try CloudFormation-first discovery
95
95
  const cfDiscovery = new CloudFormationDiscovery(provider, { serviceName, stage });
@@ -135,9 +135,9 @@ async function gatherDiscoveredResources(appDefinition) {
135
135
  // KMS keys CAN be shared across stages (encryption keys are safe to reuse)
136
136
  const kmsDiscovery = new KmsDiscovery(provider);
137
137
  const kmsConfig = {
138
- serviceName: appDefinition.name || 'create-frigg-app',
138
+ serviceName: appDefinition.name || 'frigg-app',
139
139
  stage,
140
- keyAlias: `alias/${appDefinition.name || 'create-frigg-app'}-${stage}-frigg-kms`,
140
+ keyAlias: `alias/${appDefinition.name || 'frigg-app'}-${stage}-frigg-kms`,
141
141
  };
142
142
  const kmsResult = await kmsDiscovery.discover(kmsConfig);
143
143
 
@@ -166,7 +166,7 @@ async function gatherDiscoveredResources(appDefinition) {
166
166
  const ssmDiscovery = new SsmDiscovery(provider);
167
167
 
168
168
  const config = {
169
- serviceName: appDefinition.name || 'create-frigg-app',
169
+ serviceName: appDefinition.name || 'frigg-app',
170
170
  stage,
171
171
  vpcId: appDefinition.vpc?.vpcId,
172
172
  databaseId: appDefinition.database?.postgres?.clusterId ||
@@ -219,7 +219,7 @@ describe('Discovery Result Utilities', () => {
219
219
  ],
220
220
  external: [],
221
221
  fromCloudFormation: true,
222
- stackName: 'create-frigg-app-production'
222
+ stackName: 'frigg-app-production'
223
223
  };
224
224
 
225
225
  expect(discovery.fromCloudFormation).toBe(true);
@@ -165,7 +165,7 @@ function createBaseDefinition(
165
165
 
166
166
  return {
167
167
  frameworkVersion: '>=3.17.0',
168
- service: AppDefinition.name || 'create-frigg-app',
168
+ service: AppDefinition.name || 'frigg-app',
169
169
  package: {
170
170
  individually: true,
171
171
  },
@@ -311,6 +311,15 @@ function createBaseDefinition(
311
311
  { httpApi: { path: '/health/{proxy+}', method: 'GET' } },
312
312
  ],
313
313
  },
314
+ docs: {
315
+ handler: 'node_modules/@friggframework/core/handlers/routers/docs.handler',
316
+ skipEsbuild: true,
317
+ package: skipEsbuildPackageConfig,
318
+ events: [
319
+ { httpApi: { path: '/api/docs', method: 'GET' } },
320
+ { httpApi: { path: '/api/openapi.json', method: 'GET' } },
321
+ ],
322
+ },
314
323
  // Note: dbMigrate removed - MigrationBuilder now handles migration infrastructure
315
324
  // See: packages/devtools/infrastructure/domains/database/migration-builder.js
316
325
  },
@@ -30,10 +30,10 @@ describe('Base Definition Factory', () => {
30
30
  expect(result.provider.stage).toBe('${opt:stage}');
31
31
  });
32
32
 
33
- it('should default service name to create-frigg-app', () => {
33
+ it('should default service name to frigg-app', () => {
34
34
  const result = createBaseDefinition({}, {}, {});
35
35
 
36
- expect(result.service).toBe('create-frigg-app');
36
+ expect(result.service).toBe('frigg-app');
37
37
  });
38
38
 
39
39
  it('should use custom provider if specified', () => {
@@ -157,7 +157,7 @@ describe('composeServerlessDefinition', () => {
157
157
 
158
158
  const result = await composeServerlessDefinition(appDefinition);
159
159
 
160
- expect(result.service).toBe('create-frigg-app');
160
+ expect(result.service).toBe('frigg-app');
161
161
  });
162
162
 
163
163
  it('should use custom provider when specified', async () => {
@@ -1859,7 +1859,7 @@ describe('composeServerlessDefinition', () => {
1859
1859
 
1860
1860
  await expect(composeServerlessDefinition(appDefinition)).resolves.not.toThrow();
1861
1861
  const result = await composeServerlessDefinition(appDefinition);
1862
- expect(result.service).toBe('create-frigg-app');
1862
+ expect(result.service).toBe('frigg-app');
1863
1863
  });
1864
1864
 
1865
1865
  it('should handle null/undefined integrations', async () => {