@friggframework/devtools 2.0.0-next.1 → 2.0.0-next.100

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 (235) hide show
  1. package/frigg-cli/README.md +1289 -0
  2. package/frigg-cli/__tests__/unit/commands/build.test.js +279 -0
  3. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +649 -0
  4. package/frigg-cli/__tests__/unit/commands/deploy.test.js +320 -0
  5. package/frigg-cli/__tests__/unit/commands/doctor.test.js +309 -0
  6. package/frigg-cli/__tests__/unit/commands/install.test.js +400 -0
  7. package/frigg-cli/__tests__/unit/commands/ui.test.js +346 -0
  8. package/frigg-cli/__tests__/unit/dependencies.test.js +74 -0
  9. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +397 -0
  10. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +345 -0
  11. package/frigg-cli/__tests__/unit/version-detection.test.js +171 -0
  12. package/frigg-cli/__tests__/utils/mock-factory.js +270 -0
  13. package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
  14. package/frigg-cli/__tests__/utils/test-fixtures.js +463 -0
  15. package/frigg-cli/__tests__/utils/test-setup.js +287 -0
  16. package/frigg-cli/auth-command/CLAUDE.md +293 -0
  17. package/frigg-cli/auth-command/README.md +450 -0
  18. package/frigg-cli/auth-command/api-key-flow.js +153 -0
  19. package/frigg-cli/auth-command/auth-tester.js +344 -0
  20. package/frigg-cli/auth-command/credential-storage.js +182 -0
  21. package/frigg-cli/auth-command/index.js +256 -0
  22. package/frigg-cli/auth-command/json-schema-form.js +67 -0
  23. package/frigg-cli/auth-command/module-loader.js +172 -0
  24. package/frigg-cli/auth-command/oauth-callback-server.js +431 -0
  25. package/frigg-cli/auth-command/oauth-flow.js +195 -0
  26. package/frigg-cli/auth-command/utils/browser.js +30 -0
  27. package/frigg-cli/build-command/index.js +66 -0
  28. package/frigg-cli/db-setup-command/index.js +246 -0
  29. package/frigg-cli/deploy-command/SPEC-DEPLOY-DRY-RUN.md +981 -0
  30. package/frigg-cli/deploy-command/index.js +305 -0
  31. package/frigg-cli/doctor-command/index.js +335 -0
  32. package/frigg-cli/generate-command/__tests__/generate-command.test.js +301 -0
  33. package/frigg-cli/generate-command/azure-generator.js +43 -0
  34. package/frigg-cli/generate-command/gcp-generator.js +47 -0
  35. package/frigg-cli/generate-command/index.js +332 -0
  36. package/frigg-cli/generate-command/terraform-generator.js +555 -0
  37. package/frigg-cli/generate-iam-command.js +118 -0
  38. package/frigg-cli/index.js +189 -1
  39. package/frigg-cli/index.test.js +1 -4
  40. package/frigg-cli/init-command/backend-first-handler.js +756 -0
  41. package/frigg-cli/init-command/index.js +93 -0
  42. package/frigg-cli/init-command/template-handler.js +143 -0
  43. package/frigg-cli/install-command/index.js +1 -4
  44. package/frigg-cli/jest.config.js +124 -0
  45. package/frigg-cli/package.json +63 -0
  46. package/frigg-cli/repair-command/index.js +564 -0
  47. package/frigg-cli/start-command/index.js +125 -6
  48. package/frigg-cli/start-command/start-command.test.js +297 -0
  49. package/frigg-cli/test/init-command.test.js +180 -0
  50. package/frigg-cli/test/npm-registry.test.js +319 -0
  51. package/frigg-cli/ui-command/index.js +154 -0
  52. package/frigg-cli/utils/app-resolver.js +319 -0
  53. package/frigg-cli/utils/backend-path.js +16 -17
  54. package/frigg-cli/utils/database-validator.js +167 -0
  55. package/frigg-cli/utils/error-messages.js +329 -0
  56. package/frigg-cli/utils/npm-registry.js +167 -0
  57. package/frigg-cli/utils/process-manager.js +199 -0
  58. package/frigg-cli/utils/repo-detection.js +405 -0
  59. package/index.js +4 -2
  60. package/infrastructure/ARCHITECTURE.md +487 -0
  61. package/infrastructure/CLAUDE.md +481 -0
  62. package/infrastructure/HEALTH.md +468 -0
  63. package/infrastructure/README.md +522 -0
  64. package/infrastructure/__tests__/fixtures/mock-aws-resources.js +391 -0
  65. package/infrastructure/__tests__/helpers/test-utils.js +277 -0
  66. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  67. package/infrastructure/__tests__/template-generation.test.js +687 -0
  68. package/infrastructure/create-frigg-infrastructure.js +147 -0
  69. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  70. package/infrastructure/docs/PRE-DEPLOYMENT-HEALTH-CHECK-SPEC.md +1317 -0
  71. package/infrastructure/docs/WEBSOCKET-CONFIGURATION.md +105 -0
  72. package/infrastructure/docs/deployment-instructions.md +268 -0
  73. package/infrastructure/docs/generate-iam-command.md +278 -0
  74. package/infrastructure/docs/iam-policy-templates.md +193 -0
  75. package/infrastructure/domains/admin-scripts/admin-script-builder.js +330 -0
  76. package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +628 -0
  77. package/infrastructure/domains/admin-scripts/index.js +5 -0
  78. package/infrastructure/domains/database/aurora-builder.js +857 -0
  79. package/infrastructure/domains/database/aurora-builder.test.js +960 -0
  80. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  81. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  82. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  83. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  84. package/infrastructure/domains/database/migration-builder.js +715 -0
  85. package/infrastructure/domains/database/migration-builder.test.js +322 -0
  86. package/infrastructure/domains/database/migration-resolver.js +163 -0
  87. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  88. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  89. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  90. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  91. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  92. package/infrastructure/domains/health/application/ports/index.js +26 -0
  93. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  94. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  95. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  96. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  97. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  98. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  99. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  100. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  101. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  102. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  103. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  104. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  105. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  106. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  107. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  108. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  109. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  110. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  111. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  112. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  113. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  114. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  115. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  116. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  117. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  118. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  119. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  120. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  121. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  122. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  123. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  124. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  125. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  126. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  127. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  128. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  129. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  130. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  131. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  132. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  133. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  134. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  135. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  136. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  137. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  138. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  139. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  140. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  141. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  142. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  143. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  144. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  145. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  146. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  147. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  148. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  149. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  150. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  151. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  152. package/infrastructure/domains/integration/integration-builder.js +596 -0
  153. package/infrastructure/domains/integration/integration-builder.test.js +939 -0
  154. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  155. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  156. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  157. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  158. package/infrastructure/domains/networking/vpc-builder.js +2051 -0
  159. package/infrastructure/domains/networking/vpc-builder.test.js +1960 -0
  160. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  161. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  162. package/infrastructure/domains/networking/vpc-resolver.js +505 -0
  163. package/infrastructure/domains/networking/vpc-resolver.test.js +801 -0
  164. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  165. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  166. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  167. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  168. package/infrastructure/domains/scheduler/scheduler-builder.js +211 -0
  169. package/infrastructure/domains/security/iam-generator.js +816 -0
  170. package/infrastructure/domains/security/iam-generator.test.js +204 -0
  171. package/infrastructure/domains/security/kms-builder.js +415 -0
  172. package/infrastructure/domains/security/kms-builder.test.js +392 -0
  173. package/infrastructure/domains/security/kms-discovery.js +80 -0
  174. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  175. package/infrastructure/domains/security/kms-resolver.js +96 -0
  176. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  177. package/infrastructure/domains/security/templates/frigg-deployment-iam-stack.yaml +401 -0
  178. package/infrastructure/domains/security/templates/iam-policy-basic.json +218 -0
  179. package/infrastructure/domains/security/templates/iam-policy-full.json +288 -0
  180. package/infrastructure/domains/shared/base-builder.js +112 -0
  181. package/infrastructure/domains/shared/base-resolver.js +186 -0
  182. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  183. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  184. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  185. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  186. package/infrastructure/domains/shared/cloudformation-discovery.js +681 -0
  187. package/infrastructure/domains/shared/cloudformation-discovery.test.js +1320 -0
  188. package/infrastructure/domains/shared/environment-builder.js +119 -0
  189. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  190. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +579 -0
  191. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +416 -0
  192. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  193. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  194. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  195. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  196. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  197. package/infrastructure/domains/shared/resource-discovery.enhanced.test.js +306 -0
  198. package/infrastructure/domains/shared/resource-discovery.js +256 -0
  199. package/infrastructure/domains/shared/resource-discovery.test.js +757 -0
  200. package/infrastructure/domains/shared/types/app-definition.js +225 -0
  201. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  202. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  203. package/infrastructure/domains/shared/types/index.js +46 -0
  204. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  205. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  206. package/infrastructure/domains/shared/utilities/base-definition-factory.js +418 -0
  207. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  208. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +291 -0
  209. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  210. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  211. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +159 -0
  212. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +444 -0
  213. package/infrastructure/domains/shared/validation/env-validator.js +78 -0
  214. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  215. package/infrastructure/domains/shared/validation/plugin-validator.js +187 -0
  216. package/infrastructure/domains/shared/validation/plugin-validator.test.js +323 -0
  217. package/infrastructure/esbuild.config.js +53 -0
  218. package/infrastructure/index.js +4 -0
  219. package/infrastructure/infrastructure-composer.js +121 -0
  220. package/infrastructure/infrastructure-composer.test.js +1896 -0
  221. package/infrastructure/integration.test.js +383 -0
  222. package/infrastructure/scripts/build-prisma-layer.js +701 -0
  223. package/infrastructure/scripts/build-prisma-layer.test.js +170 -0
  224. package/infrastructure/scripts/build-time-discovery.js +238 -0
  225. package/infrastructure/scripts/build-time-discovery.test.js +379 -0
  226. package/infrastructure/scripts/run-discovery.js +110 -0
  227. package/infrastructure/scripts/verify-prisma-layer.js +72 -0
  228. package/management-ui/README.md +203 -0
  229. package/package.json +53 -11
  230. package/test/index.js +2 -4
  231. package/test/mock-api.js +1 -3
  232. package/test/mock-integration.js +4 -14
  233. package/.eslintrc.json +0 -3
  234. package/CHANGELOG.md +0 -132
  235. package/test/auther-definition-tester.js +0 -125
@@ -0,0 +1,628 @@
1
+ /**
2
+ * Tests for Admin Script Builder
3
+ *
4
+ * Tests admin script infrastructure generation including:
5
+ * - SQS queue for script execution
6
+ * - Lambda executor function
7
+ * - Lambda router function with HTTP routes
8
+ * - EventBridge Scheduler resources (optional)
9
+ */
10
+
11
+ const { AdminScriptBuilder } = require('./admin-script-builder');
12
+ const { ValidationResult } = require('../shared/base-builder');
13
+
14
+ describe('AdminScriptBuilder', () => {
15
+ let adminScriptBuilder;
16
+
17
+ beforeEach(() => {
18
+ adminScriptBuilder = new AdminScriptBuilder();
19
+ });
20
+
21
+ describe('shouldExecute()', () => {
22
+ it('should return false when no adminScripts', () => {
23
+ const appDefinition = {};
24
+
25
+ expect(adminScriptBuilder.shouldExecute(appDefinition)).toBe(false);
26
+ });
27
+
28
+ it('should return false when adminScripts is empty array', () => {
29
+ const appDefinition = {
30
+ adminScripts: [],
31
+ };
32
+
33
+ expect(adminScriptBuilder.shouldExecute(appDefinition)).toBe(false);
34
+ });
35
+
36
+ it('should return true when adminScripts has items', () => {
37
+ const appDefinition = {
38
+ adminScripts: [
39
+ { Definition: { name: 'test-script' } },
40
+ ],
41
+ };
42
+
43
+ expect(adminScriptBuilder.shouldExecute(appDefinition)).toBe(true);
44
+ });
45
+
46
+ it('should return false when adminScripts is not an array', () => {
47
+ const appDefinition = {
48
+ adminScripts: { name: 'test' },
49
+ };
50
+
51
+ expect(adminScriptBuilder.shouldExecute(appDefinition)).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe('getDependencies()', () => {
56
+ it('should have no dependencies', () => {
57
+ const deps = adminScriptBuilder.getDependencies();
58
+
59
+ expect(deps).toEqual([]);
60
+ });
61
+ });
62
+
63
+ describe('validate()', () => {
64
+ it('should pass validation with valid adminScripts', () => {
65
+ const appDefinition = {
66
+ adminScripts: [
67
+ { Definition: { name: 'oauth-refresh' } },
68
+ { Definition: { name: 'health-check' } },
69
+ ],
70
+ };
71
+
72
+ const result = adminScriptBuilder.validate(appDefinition);
73
+
74
+ expect(result).toBeInstanceOf(ValidationResult);
75
+ expect(result.valid).toBe(true);
76
+ expect(result.errors).toEqual([]);
77
+ });
78
+
79
+ it('should pass when adminScripts is undefined', () => {
80
+ const appDefinition = {};
81
+
82
+ const result = adminScriptBuilder.validate(appDefinition);
83
+
84
+ expect(result.valid).toBe(true);
85
+ });
86
+
87
+ it('should fail when adminScripts is not an array', () => {
88
+ const appDefinition = {
89
+ adminScripts: 'invalid',
90
+ };
91
+
92
+ const result = adminScriptBuilder.validate(appDefinition);
93
+
94
+ expect(result.valid).toBe(false);
95
+ expect(result.errors).toContain('adminScripts must be an array');
96
+ });
97
+
98
+ it('should fail when script missing Definition.name', () => {
99
+ const appDefinition = {
100
+ adminScripts: [
101
+ { Definition: {} },
102
+ ],
103
+ };
104
+
105
+ const result = adminScriptBuilder.validate(appDefinition);
106
+
107
+ expect(result.valid).toBe(false);
108
+ expect(result.errors).toContain(
109
+ 'Admin script at index 0 is missing Definition or name'
110
+ );
111
+ });
112
+
113
+ it('should fail when script missing Definition', () => {
114
+ const appDefinition = {
115
+ adminScripts: [
116
+ { someOtherField: 'value' },
117
+ ],
118
+ };
119
+
120
+ const result = adminScriptBuilder.validate(appDefinition);
121
+
122
+ expect(result.valid).toBe(false);
123
+ expect(result.errors).toContain(
124
+ 'Admin script at index 0 is missing Definition or name'
125
+ );
126
+ });
127
+
128
+ it('should validate all scripts', () => {
129
+ const appDefinition = {
130
+ adminScripts: [
131
+ { Definition: { name: 'valid' } },
132
+ { Definition: {} }, // Invalid - no name
133
+ { someField: 'value' }, // Invalid - no Definition
134
+ ],
135
+ };
136
+
137
+ const result = adminScriptBuilder.validate(appDefinition);
138
+
139
+ expect(result.valid).toBe(false);
140
+ expect(result.errors).toHaveLength(2);
141
+ });
142
+ });
143
+
144
+ describe('build()', () => {
145
+ it('should create AdminScriptQueue resource', async () => {
146
+ const appDefinition = {
147
+ adminScripts: [
148
+ { Definition: { name: 'test-script' } },
149
+ ],
150
+ };
151
+
152
+ const result = await adminScriptBuilder.build(appDefinition, {});
153
+
154
+ expect(result.resources.AdminScriptQueue).toBeDefined();
155
+ expect(result.resources.AdminScriptQueue.Type).toBe('AWS::SQS::Queue');
156
+ });
157
+
158
+ it('should configure AdminScriptQueue with correct retention and timeout', async () => {
159
+ const appDefinition = {
160
+ adminScripts: [
161
+ { Definition: { name: 'test-script' } },
162
+ ],
163
+ };
164
+
165
+ const result = await adminScriptBuilder.build(appDefinition, {});
166
+
167
+ expect(result.resources.AdminScriptQueue.Properties.MessageRetentionPeriod).toBe(86400); // 1 day
168
+ expect(result.resources.AdminScriptQueue.Properties.VisibilityTimeout).toBe(900); // 15 minutes
169
+ });
170
+
171
+ it('should configure AdminScriptQueue redrive policy to InternalErrorQueue', async () => {
172
+ const appDefinition = {
173
+ adminScripts: [
174
+ { Definition: { name: 'test-script' } },
175
+ ],
176
+ };
177
+
178
+ const result = await adminScriptBuilder.build(appDefinition, {});
179
+
180
+ expect(result.resources.AdminScriptQueue.Properties.RedrivePolicy).toEqual({
181
+ maxReceiveCount: 3,
182
+ deadLetterTargetArn: {
183
+ 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
184
+ },
185
+ });
186
+ });
187
+
188
+ it('should add ADMIN_SCRIPT_QUEUE_URL to environment variables', async () => {
189
+ const appDefinition = {
190
+ adminScripts: [
191
+ { Definition: { name: 'test-script' } },
192
+ ],
193
+ };
194
+
195
+ const result = await adminScriptBuilder.build(appDefinition, {});
196
+
197
+ expect(result.environment.ADMIN_SCRIPT_QUEUE_URL).toEqual({
198
+ Ref: 'AdminScriptQueue',
199
+ });
200
+ });
201
+
202
+ it('should create adminScriptExecutor function', async () => {
203
+ const appDefinition = {
204
+ adminScripts: [
205
+ { Definition: { name: 'test-script' } },
206
+ ],
207
+ };
208
+
209
+ const result = await adminScriptBuilder.build(appDefinition, {});
210
+
211
+ expect(result.functions.adminScriptExecutor).toBeDefined();
212
+ expect(result.functions.adminScriptExecutor.handler).toBe(
213
+ 'node_modules/@friggframework/admin-scripts/src/infrastructure/script-executor-handler.handler'
214
+ );
215
+ });
216
+
217
+ it('should configure adminScriptExecutor with SQS event', async () => {
218
+ const appDefinition = {
219
+ adminScripts: [
220
+ { Definition: { name: 'test-script' } },
221
+ ],
222
+ };
223
+
224
+ const result = await adminScriptBuilder.build(appDefinition, {});
225
+
226
+ expect(result.functions.adminScriptExecutor.events).toEqual([
227
+ {
228
+ sqs: {
229
+ arn: { 'Fn::GetAtt': ['AdminScriptQueue', 'Arn'] },
230
+ batchSize: 1,
231
+ },
232
+ },
233
+ ]);
234
+ });
235
+
236
+ it('should set adminScriptExecutor timeout to 900 seconds', async () => {
237
+ const appDefinition = {
238
+ adminScripts: [
239
+ { Definition: { name: 'test-script' } },
240
+ ],
241
+ };
242
+
243
+ const result = await adminScriptBuilder.build(appDefinition, {});
244
+
245
+ expect(result.functions.adminScriptExecutor.timeout).toBe(900); // 15 minutes (Lambda max)
246
+ });
247
+
248
+ it('should set adminScriptExecutor memory size', async () => {
249
+ const appDefinition = {
250
+ adminScripts: [
251
+ { Definition: { name: 'test-script' } },
252
+ ],
253
+ };
254
+
255
+ const result = await adminScriptBuilder.build(appDefinition, {});
256
+
257
+ expect(result.functions.adminScriptExecutor.memorySize).toBe(1024);
258
+ });
259
+
260
+ it('should exclude aws-sdk/Prisma from the skipEsbuild function packages', async () => {
261
+ const appDefinition = {
262
+ adminScripts: [{ Definition: { name: 'test-script' } }],
263
+ };
264
+
265
+ const result = await adminScriptBuilder.build(appDefinition, {});
266
+
267
+ // Regression: without a package config the skipEsbuild functions
268
+ // package the whole node_modules and exceed Lambda's 250 MB limit.
269
+ for (const fn of [
270
+ result.functions.adminScriptExecutor,
271
+ result.functions.adminScriptRouter,
272
+ ]) {
273
+ expect(fn.package.exclude).toEqual(
274
+ expect.arrayContaining([
275
+ 'node_modules/aws-sdk/**',
276
+ 'node_modules/@aws-sdk/**',
277
+ 'node_modules/@prisma/**',
278
+ ])
279
+ );
280
+ }
281
+ });
282
+
283
+ it('should attach Prisma layer to adminScriptExecutor', async () => {
284
+ const appDefinition = {
285
+ adminScripts: [
286
+ { Definition: { name: 'test-script' } },
287
+ ],
288
+ };
289
+
290
+ const result = await adminScriptBuilder.build(appDefinition, {});
291
+
292
+ expect(result.functions.adminScriptExecutor.layers).toEqual([
293
+ { Ref: 'PrismaLambdaLayer' }
294
+ ]);
295
+ });
296
+
297
+ it('should create adminScriptRouter function', async () => {
298
+ const appDefinition = {
299
+ adminScripts: [
300
+ { Definition: { name: 'test-script' } },
301
+ ],
302
+ };
303
+
304
+ const result = await adminScriptBuilder.build(appDefinition, {});
305
+
306
+ expect(result.functions.adminScriptRouter).toBeDefined();
307
+ expect(result.functions.adminScriptRouter.handler).toBe(
308
+ 'node_modules/@friggframework/admin-scripts/src/infrastructure/admin-script-router.handler'
309
+ );
310
+ });
311
+
312
+ it('should configure adminScriptRouter with correct HTTP routes', async () => {
313
+ const appDefinition = {
314
+ adminScripts: [
315
+ { Definition: { name: 'test-script' } },
316
+ ],
317
+ };
318
+
319
+ const result = await adminScriptBuilder.build(appDefinition, {});
320
+
321
+ expect(result.functions.adminScriptRouter.events).toEqual([
322
+ // List scripts
323
+ { httpApi: { path: '/admin/scripts', method: 'GET' } },
324
+ // Get script details
325
+ { httpApi: { path: '/admin/scripts/{scriptName}', method: 'GET' } },
326
+ // Execute script (sync or async)
327
+ { httpApi: { path: '/admin/scripts/{scriptName}', method: 'POST' } },
328
+ // Validate script input (dry-run preview)
329
+ { httpApi: { path: '/admin/scripts/{scriptName}/validate', method: 'POST' } },
330
+ // List executions for a script
331
+ { httpApi: { path: '/admin/scripts/{scriptName}/executions', method: 'GET' } },
332
+ // Get a single execution
333
+ {
334
+ httpApi: {
335
+ path: '/admin/scripts/{scriptName}/executions/{executionId}',
336
+ method: 'GET',
337
+ },
338
+ },
339
+ // Schedule management (Phase 2)
340
+ { httpApi: { path: '/admin/scripts/{scriptName}/schedule', method: 'GET' } },
341
+ { httpApi: { path: '/admin/scripts/{scriptName}/schedule', method: 'PUT' } },
342
+ { httpApi: { path: '/admin/scripts/{scriptName}/schedule', method: 'DELETE' } },
343
+ ]);
344
+ });
345
+
346
+ it('should set adminScriptRouter timeout to 30 seconds', async () => {
347
+ const appDefinition = {
348
+ adminScripts: [
349
+ { Definition: { name: 'test-script' } },
350
+ ],
351
+ };
352
+
353
+ const result = await adminScriptBuilder.build(appDefinition, {});
354
+
355
+ expect(result.functions.adminScriptRouter.timeout).toBe(30);
356
+ });
357
+
358
+ it('should attach Prisma layer to adminScriptRouter', async () => {
359
+ const appDefinition = {
360
+ adminScripts: [
361
+ { Definition: { name: 'test-script' } },
362
+ ],
363
+ };
364
+
365
+ const result = await adminScriptBuilder.build(appDefinition, {});
366
+
367
+ expect(result.functions.adminScriptRouter.layers).toEqual([
368
+ { Ref: 'PrismaLambdaLayer' }
369
+ ]);
370
+ });
371
+
372
+ it('should create scheduler resources when admin.enableScheduling is true', async () => {
373
+ const appDefinition = {
374
+ adminScripts: [
375
+ { Definition: { name: 'test-script' } },
376
+ ],
377
+ admin: {
378
+ enableScheduling: true,
379
+ },
380
+ };
381
+
382
+ const result = await adminScriptBuilder.build(appDefinition, {});
383
+
384
+ // Check for scheduler IAM role
385
+ expect(result.resources.AdminScriptSchedulerRole).toBeDefined();
386
+ expect(result.resources.AdminScriptSchedulerRole.Type).toBe('AWS::IAM::Role');
387
+
388
+ // Check for schedule group
389
+ expect(result.resources.AdminScriptScheduleGroup).toBeDefined();
390
+ expect(result.resources.AdminScriptScheduleGroup.Type).toBe('AWS::Scheduler::ScheduleGroup');
391
+
392
+ // Scheduler env vars are scoped to the router function (not the
393
+ // shared provider env): resource refs would create CloudFormation
394
+ // circular deps, and SCHEDULER_PROVIDER='aws' would break core's
395
+ // scheduler factory (rejects 'aws') for every other Lambda.
396
+ const routerEnv = result.functions.adminScriptRouter.environment;
397
+ expect(routerEnv.SCHEDULER_PROVIDER).toBe('aws');
398
+ expect(result.environment.SCHEDULER_PROVIDER).toBeUndefined();
399
+ expect(routerEnv.SCHEDULER_ROLE_ARN).toEqual({
400
+ 'Fn::GetAtt': ['AdminScriptSchedulerRole', 'Arn'],
401
+ });
402
+ expect(routerEnv.ADMIN_SCRIPT_SCHEDULE_GROUP).toEqual({
403
+ Ref: 'AdminScriptScheduleGroup',
404
+ });
405
+ // Executor referenced by constructed ARN (Fn::Sub), not Fn::GetAtt,
406
+ // so it creates no dependency edge.
407
+ expect(routerEnv.ADMIN_SCRIPT_EXECUTOR_LAMBDA_ARN).toEqual({
408
+ 'Fn::Sub':
409
+ 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${self:service}-${self:provider.stage}-adminScriptExecutor',
410
+ });
411
+
412
+ // Regression guard: these must NOT leak onto the shared provider env
413
+ // (that is what produced the circular dependency, incl. the executor
414
+ // self-reference).
415
+ expect(result.environment.SCHEDULER_ROLE_ARN).toBeUndefined();
416
+ expect(result.environment.ADMIN_SCRIPT_SCHEDULE_GROUP).toBeUndefined();
417
+ expect(
418
+ result.environment.ADMIN_SCRIPT_EXECUTOR_LAMBDA_ARN
419
+ ).toBeUndefined();
420
+ });
421
+
422
+ it('should not create scheduler resources when enableScheduling is false', async () => {
423
+ const appDefinition = {
424
+ adminScripts: [
425
+ { Definition: { name: 'test-script' } },
426
+ ],
427
+ admin: {
428
+ enableScheduling: false,
429
+ },
430
+ };
431
+
432
+ const result = await adminScriptBuilder.build(appDefinition, {});
433
+
434
+ expect(result.resources.AdminScriptSchedulerRole).toBeUndefined();
435
+ expect(result.resources.AdminScriptScheduleGroup).toBeUndefined();
436
+ expect(result.environment.SCHEDULER_ROLE_ARN).toBeUndefined();
437
+ expect(result.environment.ADMIN_SCRIPT_SCHEDULE_GROUP).toBeUndefined();
438
+ });
439
+
440
+ it('should not create scheduler resources when admin config is not provided', async () => {
441
+ const appDefinition = {
442
+ adminScripts: [
443
+ { Definition: { name: 'test-script' } },
444
+ ],
445
+ };
446
+
447
+ const result = await adminScriptBuilder.build(appDefinition, {});
448
+
449
+ expect(result.resources.AdminScriptSchedulerRole).toBeUndefined();
450
+ expect(result.resources.AdminScriptScheduleGroup).toBeUndefined();
451
+ });
452
+
453
+ it('should use skipEsbuild for all functions', async () => {
454
+ const appDefinition = {
455
+ adminScripts: [
456
+ { Definition: { name: 'test-script' } },
457
+ ],
458
+ };
459
+
460
+ const result = await adminScriptBuilder.build(appDefinition, {});
461
+
462
+ expect(result.functions.adminScriptExecutor.skipEsbuild).toBe(true);
463
+ expect(result.functions.adminScriptRouter.skipEsbuild).toBe(true);
464
+ });
465
+
466
+ it('should not attach Prisma layer when usePrismaLambdaLayer=false', async () => {
467
+ const appDefinition = {
468
+ usePrismaLambdaLayer: false,
469
+ adminScripts: [
470
+ { Definition: { name: 'test-script' } },
471
+ ],
472
+ };
473
+
474
+ const result = await adminScriptBuilder.build(appDefinition, {});
475
+
476
+ expect(result.functions.adminScriptExecutor.layers).toBeUndefined();
477
+ expect(result.functions.adminScriptRouter.layers).toBeUndefined();
478
+ });
479
+
480
+ it('should handle multiple admin scripts', async () => {
481
+ const appDefinition = {
482
+ adminScripts: [
483
+ { Definition: { name: 'oauth-refresh' } },
484
+ { Definition: { name: 'health-check' } },
485
+ { Definition: { name: 'attio-healing' } },
486
+ ],
487
+ };
488
+
489
+ const result = await adminScriptBuilder.build(appDefinition, {});
490
+
491
+ // Should still only create one queue and two functions
492
+ expect(result.resources.AdminScriptQueue).toBeDefined();
493
+ expect(result.functions.adminScriptExecutor).toBeDefined();
494
+ expect(result.functions.adminScriptRouter).toBeDefined();
495
+
496
+ // Should not create separate resources per script
497
+ expect(Object.keys(result.resources)).toHaveLength(1); // Only AdminScriptQueue
498
+ expect(Object.keys(result.functions)).toHaveLength(2); // Only executor and router
499
+ });
500
+
501
+ it('should configure scheduler role with correct trust policy', async () => {
502
+ const appDefinition = {
503
+ adminScripts: [
504
+ { Definition: { name: 'test-script' } },
505
+ ],
506
+ admin: {
507
+ enableScheduling: true,
508
+ },
509
+ };
510
+
511
+ const result = await adminScriptBuilder.build(appDefinition, {});
512
+
513
+ const trustPolicy = result.resources.AdminScriptSchedulerRole.Properties.AssumeRolePolicyDocument;
514
+
515
+ expect(trustPolicy.Statement[0]).toEqual({
516
+ Effect: 'Allow',
517
+ Principal: { Service: 'scheduler.amazonaws.com' },
518
+ Action: 'sts:AssumeRole',
519
+ });
520
+ });
521
+
522
+ it('should configure scheduler role with Lambda invoke permission', async () => {
523
+ const appDefinition = {
524
+ adminScripts: [
525
+ { Definition: { name: 'test-script' } },
526
+ ],
527
+ admin: {
528
+ enableScheduling: true,
529
+ },
530
+ };
531
+
532
+ const result = await adminScriptBuilder.build(appDefinition, {});
533
+
534
+ const policies = result.resources.AdminScriptSchedulerRole.Properties.Policies;
535
+
536
+ expect(policies[0].PolicyName).toBe('InvokeLambda');
537
+ // Constructed ARN (Fn::Sub), not Fn::GetAtt — a GetAtt here closes a
538
+ // circular dependency through the shared Lambda execution role.
539
+ expect(policies[0].PolicyDocument.Statement[0]).toEqual({
540
+ Effect: 'Allow',
541
+ Action: 'lambda:InvokeFunction',
542
+ Resource: {
543
+ 'Fn::Sub':
544
+ 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${self:service}-${self:provider.stage}-adminScriptExecutor',
545
+ },
546
+ });
547
+ });
548
+
549
+ it('should grant the router SendMessage on AdminScriptQueue', async () => {
550
+ const appDefinition = {
551
+ adminScripts: [{ Definition: { name: 'test-script' } }],
552
+ };
553
+
554
+ const result = await adminScriptBuilder.build(appDefinition, {});
555
+
556
+ const sqsGrant = result.iamStatements.find(
557
+ (s) =>
558
+ Array.isArray(s.Action) &&
559
+ s.Action.includes('sqs:SendMessage')
560
+ );
561
+ expect(sqsGrant).toBeDefined();
562
+ expect(sqsGrant.Action).toContain('sqs:SendMessageBatch');
563
+ expect(sqsGrant.Resource).toEqual({
564
+ 'Fn::GetAtt': ['AdminScriptQueue', 'Arn'],
565
+ });
566
+ });
567
+
568
+ it('should grant scheduler:* + iam:PassRole when scheduling is enabled', async () => {
569
+ const appDefinition = {
570
+ adminScripts: [{ Definition: { name: 'test-script' } }],
571
+ admin: { enableScheduling: true },
572
+ };
573
+
574
+ const result = await adminScriptBuilder.build(appDefinition, {});
575
+
576
+ const schedulerGrant = result.iamStatements.find(
577
+ (s) =>
578
+ Array.isArray(s.Action) &&
579
+ s.Action.includes('scheduler:CreateSchedule')
580
+ );
581
+ expect(schedulerGrant).toBeDefined();
582
+ expect(schedulerGrant.Action).toEqual(
583
+ expect.arrayContaining([
584
+ 'scheduler:CreateSchedule',
585
+ 'scheduler:UpdateSchedule',
586
+ 'scheduler:DeleteSchedule',
587
+ 'scheduler:GetSchedule',
588
+ ])
589
+ );
590
+ expect(schedulerGrant.Resource['Fn::Sub'][1]).toEqual({
591
+ GroupName: { Ref: 'AdminScriptScheduleGroup' },
592
+ });
593
+
594
+ const passRole = result.iamStatements.find(
595
+ (s) =>
596
+ Array.isArray(s.Action) && s.Action.includes('iam:PassRole')
597
+ );
598
+ expect(passRole).toBeDefined();
599
+ expect(passRole.Resource).toEqual({
600
+ 'Fn::GetAtt': ['AdminScriptSchedulerRole', 'Arn'],
601
+ });
602
+ expect(passRole.Condition.StringEquals['iam:PassedToService']).toBe(
603
+ 'scheduler.amazonaws.com'
604
+ );
605
+ });
606
+
607
+ it('should add no scheduler IAM statements when scheduling is disabled', async () => {
608
+ const appDefinition = {
609
+ adminScripts: [{ Definition: { name: 'test-script' } }],
610
+ };
611
+
612
+ const result = await adminScriptBuilder.build(appDefinition, {});
613
+
614
+ const schedulerGrant = result.iamStatements.find(
615
+ (s) =>
616
+ Array.isArray(s.Action) &&
617
+ s.Action.some((a) => a.startsWith('scheduler:'))
618
+ );
619
+ expect(schedulerGrant).toBeUndefined();
620
+ });
621
+ });
622
+
623
+ describe('getName()', () => {
624
+ it('should return AdminScriptBuilder', () => {
625
+ expect(adminScriptBuilder.getName()).toBe('AdminScriptBuilder');
626
+ });
627
+ });
628
+ });
@@ -0,0 +1,5 @@
1
+ const { AdminScriptBuilder } = require('./admin-script-builder');
2
+
3
+ module.exports = {
4
+ AdminScriptBuilder
5
+ };