@friggframework/devtools 2.0.0--canary.666.81647c4.0 → 2.0.0--canary.667.6dc4b39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frigg-cli/README.md +1 -38
- package/frigg-cli/__tests__/unit/commands/build.test.js +0 -3
- package/frigg-cli/__tests__/unit/commands/deploy.test.js +0 -3
- package/frigg-cli/build-command/index.js +0 -6
- package/frigg-cli/deploy-command/index.js +0 -6
- package/frigg-cli/index.js +1 -13
- package/frigg-cli/init-command/backend-first-handler.js +46 -25
- package/frigg-cli/start-command/index.js +0 -4
- package/frigg-cli/start-command/start-command.test.js +0 -3
- package/infrastructure/__tests__/management-api-gateway-routes.test.js +281 -0
- package/infrastructure/create-frigg-infrastructure.js +1 -4
- package/infrastructure/docs/POSTGRES-CONFIGURATION.md +98 -60
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +19 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +13 -1
- package/package.json +7 -7
- package/frigg-cli/__tests__/unit/commands/validate-wiring.test.js +0 -92
- package/frigg-cli/validate-command/__tests__/init-validation.test.js +0 -63
- package/frigg-cli/validate-command/__tests__/integrations-checks.test.js +0 -133
- package/frigg-cli/validate-command/__tests__/validate-app-definition.test.js +0 -326
- package/frigg-cli/validate-command/__tests__/validate-command.test.js +0 -241
- package/frigg-cli/validate-command/checks/config.js +0 -468
- package/frigg-cli/validate-command/checks/integrations.js +0 -274
- package/frigg-cli/validate-command/format-report.js +0 -58
- package/frigg-cli/validate-command/index.js +0 -107
- package/frigg-cli/validate-command/load-app-definition.js +0 -93
- package/frigg-cli/validate-command/schema-issues.js +0 -154
- package/frigg-cli/validate-command/validate-app-definition.js +0 -60
- package/infrastructure/app-definition-schema-contract.test.js +0 -607
- package/infrastructure/create-frigg-infrastructure.test.js +0 -51
|
@@ -8,10 +8,10 @@ Frigg Framework supports automatic provisioning of Amazon Aurora Serverless v2 P
|
|
|
8
8
|
|
|
9
9
|
### Key Features
|
|
10
10
|
|
|
11
|
-
- **Aurora Serverless v2**: Cost-efficient auto-scaling database (0.5-
|
|
11
|
+
- **Aurora Serverless v2**: Cost-efficient auto-scaling database (0.5-1.0 ACU default)
|
|
12
12
|
- **VPC Integration**: Deployed in same private subnets as Lambda functions
|
|
13
13
|
- **Secrets Manager**: Automatic credential management and rotation
|
|
14
|
-
- **Three Management Modes**: discover,
|
|
14
|
+
- **Three Management Modes**: discover, create-new, use-existing
|
|
15
15
|
- **Security**: Private subnet deployment with security group isolation
|
|
16
16
|
- **High Availability**: Multi-AZ deployment with automatic failover
|
|
17
17
|
|
|
@@ -21,16 +21,12 @@ Frigg Framework supports automatic provisioning of Amazon Aurora Serverless v2 P
|
|
|
21
21
|
|
|
22
22
|
### App Definition Structure
|
|
23
23
|
|
|
24
|
-
These are the keys the Aurora builder reads (see `database.postgres` in
|
|
25
|
-
`packages/schemas/schemas/app-definition.schema.json`). `frigg validate`
|
|
26
|
-
rejects any other key under `database.postgres`.
|
|
27
|
-
|
|
28
24
|
```javascript
|
|
29
25
|
// backend/index.js
|
|
30
26
|
const appDefinition = {
|
|
31
27
|
name: 'my-frigg-app',
|
|
32
28
|
|
|
33
|
-
// VPC
|
|
29
|
+
// Enable VPC deployment (required for Aurora)
|
|
34
30
|
vpc: {
|
|
35
31
|
enable: true,
|
|
36
32
|
},
|
|
@@ -40,24 +36,34 @@ const appDefinition = {
|
|
|
40
36
|
postgres: {
|
|
41
37
|
enable: true,
|
|
42
38
|
|
|
43
|
-
// 'discover'
|
|
44
|
-
// Ignored when managementMode is 'managed' or 'existing'.
|
|
39
|
+
// Management mode: 'discover' | 'create-new' | 'use-existing'
|
|
45
40
|
management: 'discover',
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
// Basic Configuration
|
|
43
|
+
databaseName: 'frigg_db',
|
|
44
|
+
masterUsername: 'frigg_admin',
|
|
45
|
+
|
|
46
|
+
// Engine Configuration
|
|
47
|
+
engine: 'aurora-postgresql',
|
|
48
|
+
engineVersion: '15.3',
|
|
49
|
+
|
|
50
|
+
// Scaling Configuration (Aurora Serverless v2)
|
|
51
|
+
scaling: {
|
|
52
|
+
minCapacity: 0.5, // ACUs (0.5 = ~1GB RAM, ~$43/month)
|
|
53
|
+
maxCapacity: 1.0, // ACUs (1.0 = ~2GB RAM, ~$87/month)
|
|
54
|
+
},
|
|
50
55
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
// Backup Configuration
|
|
57
|
+
backupRetentionDays: 7,
|
|
58
|
+
preferredBackupWindow: '03:00-04:00',
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
// Security & Advanced
|
|
61
|
+
deletionProtection: true,
|
|
62
|
+
enablePerformanceInsights: false,
|
|
57
63
|
|
|
58
|
-
// use-existing
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
// For use-existing mode
|
|
65
|
+
clusterIdentifier: 'my-existing-cluster',
|
|
66
|
+
secretArn: 'arn:aws:secretsmanager:...',
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
};
|
|
@@ -67,17 +73,13 @@ module.exports = {
|
|
|
67
73
|
};
|
|
68
74
|
```
|
|
69
75
|
|
|
70
|
-
Backup retention (7 days), the backup and maintenance windows and deletion
|
|
71
|
-
settings are fixed by the builder and are not configurable through the app
|
|
72
|
-
definition.
|
|
73
|
-
|
|
74
76
|
---
|
|
75
77
|
|
|
76
78
|
## Management Modes
|
|
77
79
|
|
|
78
80
|
### 1. Discover Mode (Default)
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
Automatically discovers existing Aurora clusters or creates new one if none found.
|
|
81
83
|
|
|
82
84
|
```javascript
|
|
83
85
|
database: {
|
|
@@ -98,56 +100,63 @@ database: {
|
|
|
98
100
|
|
|
99
101
|
---
|
|
100
102
|
|
|
101
|
-
### 2.
|
|
103
|
+
### 2. Create-New Mode
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
Always creates a new Aurora cluster, even if existing clusters are found.
|
|
104
106
|
|
|
105
107
|
```javascript
|
|
106
108
|
database: {
|
|
107
109
|
postgres: {
|
|
108
110
|
enable: true,
|
|
109
|
-
management: '
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
management: 'create-new',
|
|
112
|
+
|
|
113
|
+
// Customization options
|
|
114
|
+
databaseName: 'my_app_db',
|
|
115
|
+
masterUsername: 'admin',
|
|
116
|
+
engineVersion: '15.3',
|
|
117
|
+
scaling: {
|
|
118
|
+
minCapacity: 1.0,
|
|
119
|
+
maxCapacity: 2.0,
|
|
120
|
+
},
|
|
121
|
+
backupRetentionDays: 14,
|
|
122
|
+
deletionProtection: true,
|
|
112
123
|
}
|
|
113
124
|
}
|
|
114
125
|
```
|
|
115
126
|
|
|
116
|
-
With the top-level `managementMode: 'managed'` (and `vpcIsolation: 'isolated'`),
|
|
117
|
-
Frigg manages the database per stage and `database.postgres.management` is ignored.
|
|
118
|
-
|
|
119
127
|
**Best For**: Production environments where you want dedicated database resources.
|
|
120
128
|
|
|
121
129
|
---
|
|
122
130
|
|
|
123
131
|
### 3. Use-Existing Mode
|
|
124
132
|
|
|
125
|
-
|
|
133
|
+
Uses a specific existing Aurora cluster by identifier.
|
|
126
134
|
|
|
127
135
|
```javascript
|
|
128
136
|
database: {
|
|
129
137
|
postgres: {
|
|
130
138
|
enable: true,
|
|
131
139
|
management: 'use-existing',
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
|
|
141
|
+
// Required: existing cluster identifier
|
|
142
|
+
clusterIdentifier: 'my-existing-aurora-cluster',
|
|
143
|
+
|
|
144
|
+
// Optional: secret ARN (discovered if not provided)
|
|
145
|
+
secretArn: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-db-secret',
|
|
146
|
+
|
|
147
|
+
// Database name to connect to
|
|
148
|
+
databaseName: 'frigg_db',
|
|
136
149
|
}
|
|
137
150
|
}
|
|
138
151
|
```
|
|
139
152
|
|
|
140
|
-
To use an existing cluster by identifier instead, use the ownership form:
|
|
141
|
-
`ownership: { cluster: 'external', instance: 'external' }` with
|
|
142
|
-
`external: { clusterIdentifier, instanceIdentifier, secretArn }`.
|
|
143
|
-
|
|
144
153
|
**Best For**: Shared database scenarios or when you manage Aurora outside of Frigg.
|
|
145
154
|
|
|
146
155
|
---
|
|
147
156
|
|
|
148
157
|
## Created AWS Resources
|
|
149
158
|
|
|
150
|
-
When provisioning Aurora (`
|
|
159
|
+
When provisioning Aurora (`create-new` or `discover` mode without existing cluster), Frigg creates:
|
|
151
160
|
|
|
152
161
|
### 1. RDS DB Subnet Group
|
|
153
162
|
- **Name**: `{service}-{stage}-db-subnet-group`
|
|
@@ -212,15 +221,17 @@ database: {
|
|
|
212
221
|
database: {
|
|
213
222
|
postgres: {
|
|
214
223
|
enable: true,
|
|
215
|
-
|
|
216
|
-
|
|
224
|
+
scaling: {
|
|
225
|
+
minCapacity: 1.0, // Higher baseline for production
|
|
226
|
+
maxCapacity: 4.0, // Handle traffic spikes
|
|
227
|
+
},
|
|
228
|
+
backupRetentionDays: 30, // Longer retention
|
|
229
|
+
enablePerformanceInsights: true, // Monitoring
|
|
230
|
+
deletionProtection: true, // Prevent accidental deletion
|
|
217
231
|
}
|
|
218
232
|
}
|
|
219
233
|
```
|
|
220
234
|
|
|
221
|
-
Backup retention, Performance Insights and deletion protection are not
|
|
222
|
-
configurable through the app definition; change them on the cluster.
|
|
223
|
-
|
|
224
235
|
**Estimated Monthly Costs**:
|
|
225
236
|
- **Baseline**: $87/month (1.0 ACU minimum)
|
|
226
237
|
- **Peak Traffic**: $348/month (4.0 ACU maximum)
|
|
@@ -265,9 +276,15 @@ const users = await prismaClient.user.findMany();
|
|
|
265
276
|
|
|
266
277
|
### 4. Deletion Protection
|
|
267
278
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
279
|
+
```javascript
|
|
280
|
+
database: {
|
|
281
|
+
postgres: {
|
|
282
|
+
deletionProtection: true, // Prevents accidental deletion
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Important**: When enabled, you must manually disable deletion protection in AWS console before stack deletion.
|
|
271
288
|
|
|
272
289
|
---
|
|
273
290
|
|
|
@@ -367,7 +384,7 @@ DB_TYPE=postgresql
|
|
|
367
384
|
database: {
|
|
368
385
|
postgres: {
|
|
369
386
|
enable: true,
|
|
370
|
-
management: '
|
|
387
|
+
management: 'create-new',
|
|
371
388
|
}
|
|
372
389
|
}
|
|
373
390
|
```
|
|
@@ -394,7 +411,7 @@ DB_TYPE=postgresql
|
|
|
394
411
|
database: {
|
|
395
412
|
postgres: {
|
|
396
413
|
enable: true,
|
|
397
|
-
management: '
|
|
414
|
+
management: 'create-new',
|
|
398
415
|
}
|
|
399
416
|
}
|
|
400
417
|
```
|
|
@@ -439,13 +456,13 @@ DB_TYPE=postgresql
|
|
|
439
456
|
|
|
440
457
|
**Error**:
|
|
441
458
|
```
|
|
442
|
-
No Aurora cluster found in discovery mode. Set management to "
|
|
459
|
+
No Aurora cluster found in discovery mode. Set management to "create-new"...
|
|
443
460
|
```
|
|
444
461
|
|
|
445
462
|
**Solution**:
|
|
446
463
|
1. Check VPC is enabled: `vpc.enable: true`
|
|
447
|
-
2. Set management mode: `management: '
|
|
448
|
-
3. Or
|
|
464
|
+
2. Set management mode: `management: 'create-new'`
|
|
465
|
+
3. Or provide cluster identifier: `clusterIdentifier: 'my-cluster'`
|
|
449
466
|
|
|
450
467
|
---
|
|
451
468
|
|
|
@@ -527,19 +544,40 @@ Cannot create Aurora cluster: InsufficientDBInstanceCapacity
|
|
|
527
544
|
|
|
528
545
|
## Advanced Configuration
|
|
529
546
|
|
|
530
|
-
### Custom
|
|
547
|
+
### Custom Backup Window
|
|
531
548
|
|
|
532
549
|
```javascript
|
|
533
550
|
database: {
|
|
534
551
|
postgres: {
|
|
535
552
|
enable: true,
|
|
536
|
-
|
|
553
|
+
backupRetentionDays: 30,
|
|
554
|
+
preferredBackupWindow: '02:00-03:00', // UTC
|
|
537
555
|
}
|
|
538
556
|
}
|
|
539
557
|
```
|
|
540
558
|
|
|
541
|
-
|
|
542
|
-
|
|
559
|
+
### Enhanced Monitoring
|
|
560
|
+
|
|
561
|
+
```javascript
|
|
562
|
+
database: {
|
|
563
|
+
postgres: {
|
|
564
|
+
enable: true,
|
|
565
|
+
enablePerformanceInsights: true,
|
|
566
|
+
// Performance Insights retention: 7 days (default) or 731 days
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Custom Engine Version
|
|
572
|
+
|
|
573
|
+
```javascript
|
|
574
|
+
database: {
|
|
575
|
+
postgres: {
|
|
576
|
+
enable: true,
|
|
577
|
+
engineVersion: '14.6', // Default: 15.3
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
```
|
|
543
581
|
|
|
544
582
|
### Read Replicas (Not Supported Yet)
|
|
545
583
|
|
|
@@ -286,6 +286,11 @@ function createBaseDefinition(
|
|
|
286
286
|
...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }),
|
|
287
287
|
skipEsbuild: true, // Handlers in node_modules don't need bundling
|
|
288
288
|
package: skipEsbuildPackageConfig,
|
|
289
|
+
// Every path prefix the core integration router
|
|
290
|
+
// (createIntegrationRouter) registers must be routed here;
|
|
291
|
+
// HTTP API has no implicit catch-all, so an unlisted prefix
|
|
292
|
+
// 404s at the gateway before Express sees it. Guarded by
|
|
293
|
+
// __tests__/management-api-gateway-routes.test.js.
|
|
289
294
|
events: [
|
|
290
295
|
{ httpApi: { path: '/api/integrations', method: 'ANY' } },
|
|
291
296
|
{
|
|
@@ -295,6 +300,20 @@ function createBaseDefinition(
|
|
|
295
300
|
},
|
|
296
301
|
},
|
|
297
302
|
{ httpApi: { path: '/api/authorize', method: 'ANY' } },
|
|
303
|
+
{ httpApi: { path: '/api/entity', method: 'ANY' } },
|
|
304
|
+
{
|
|
305
|
+
httpApi: {
|
|
306
|
+
path: '/api/entity/{proxy+}',
|
|
307
|
+
method: 'ANY',
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
{ httpApi: { path: '/api/entities', method: 'ANY' } },
|
|
311
|
+
{
|
|
312
|
+
httpApi: {
|
|
313
|
+
path: '/api/entities/{proxy+}',
|
|
314
|
+
method: 'ANY',
|
|
315
|
+
},
|
|
316
|
+
},
|
|
298
317
|
],
|
|
299
318
|
},
|
|
300
319
|
user: {
|
|
@@ -94,7 +94,19 @@ describe('Base Definition Factory', () => {
|
|
|
94
94
|
|
|
95
95
|
expect(result.functions.auth.handler).toBe('node_modules/@friggframework/core/handlers/routers/auth.handler');
|
|
96
96
|
expect(result.functions.auth.layers).toEqual([{ Ref: 'PrismaLambdaLayer' }]);
|
|
97
|
-
expect(
|
|
97
|
+
expect(
|
|
98
|
+
result.functions.auth.events.map(
|
|
99
|
+
(e) => `${e.httpApi.method} ${e.httpApi.path}`
|
|
100
|
+
)
|
|
101
|
+
).toEqual([
|
|
102
|
+
'ANY /api/integrations',
|
|
103
|
+
'ANY /api/integrations/{proxy+}',
|
|
104
|
+
'ANY /api/authorize',
|
|
105
|
+
'ANY /api/entity',
|
|
106
|
+
'ANY /api/entity/{proxy+}',
|
|
107
|
+
'ANY /api/entities',
|
|
108
|
+
'ANY /api/entities/{proxy+}',
|
|
109
|
+
]);
|
|
98
110
|
});
|
|
99
111
|
|
|
100
112
|
it('should NOT include legacy dbMigrate function', () => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/devtools",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.667.6dc4b39.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"frigg": "./frigg-cli/index.js"
|
|
7
7
|
},
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"@babel/eslint-parser": "^7.18.9",
|
|
27
27
|
"@babel/parser": "^7.25.3",
|
|
28
28
|
"@babel/traverse": "^7.25.3",
|
|
29
|
-
"@friggframework/core": "2.0.0--canary.
|
|
30
|
-
"@friggframework/schemas": "2.0.0--canary.
|
|
31
|
-
"@friggframework/test": "2.0.0--canary.
|
|
29
|
+
"@friggframework/core": "2.0.0--canary.667.6dc4b39.0",
|
|
30
|
+
"@friggframework/schemas": "2.0.0--canary.667.6dc4b39.0",
|
|
31
|
+
"@friggframework/test": "2.0.0--canary.667.6dc4b39.0",
|
|
32
32
|
"@hapi/boom": "^10.0.1",
|
|
33
33
|
"@inquirer/prompts": "^5.3.8",
|
|
34
34
|
"axios": "^1.18.0",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"validate-npm-package-name": "^5.0.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
60
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
59
|
+
"@friggframework/eslint-config": "2.0.0--canary.667.6dc4b39.0",
|
|
60
|
+
"@friggframework/prettier-config": "2.0.0--canary.667.6dc4b39.0",
|
|
61
61
|
"aws-sdk-client-mock": "^4.1.0",
|
|
62
62
|
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
63
63
|
"jest": "^30.1.3",
|
|
@@ -89,5 +89,5 @@
|
|
|
89
89
|
"publishConfig": {
|
|
90
90
|
"access": "public"
|
|
91
91
|
},
|
|
92
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "6dc4b398e9de7b24d84754021d06e948ebcf7431"
|
|
93
93
|
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* build and deploy validate the app definition first and stop on errors;
|
|
3
|
-
* start validates and only warns (ADR-051).
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// child_process and node:child_process are one module to jest: one mock.
|
|
7
|
-
const mockChildProcess = {
|
|
8
|
-
spawnSync: jest.fn(() => ({ status: 0 })),
|
|
9
|
-
spawn: jest.fn(),
|
|
10
|
-
};
|
|
11
|
-
jest.mock('child_process', () => mockChildProcess);
|
|
12
|
-
jest.mock('node:child_process', () => mockChildProcess);
|
|
13
|
-
jest.mock('../../../validate-command', () => ({
|
|
14
|
-
preflightValidation: jest.fn(),
|
|
15
|
-
}));
|
|
16
|
-
jest.mock('../../../doctor-command', () => ({ doctorCommand: jest.fn() }));
|
|
17
|
-
jest.mock('../../../utils/database-validator', () => ({
|
|
18
|
-
validateDatabaseUrl: jest.fn(() => ({ valid: true })),
|
|
19
|
-
getDatabaseType: jest.fn(() => ({ dbType: 'postgresql' })),
|
|
20
|
-
checkPrismaClientGenerated: jest.fn(() => ({ generated: true })),
|
|
21
|
-
}));
|
|
22
|
-
jest.mock('dotenv', () => ({ config: jest.fn() }));
|
|
23
|
-
|
|
24
|
-
const childProcess = mockChildProcess;
|
|
25
|
-
const nodeChildProcess = mockChildProcess;
|
|
26
|
-
const { preflightValidation } = require('../../../validate-command');
|
|
27
|
-
const { buildCommand } = require('../../../build-command');
|
|
28
|
-
const { deployCommand } = require('../../../deploy-command');
|
|
29
|
-
const { startCommand } = require('../../../start-command');
|
|
30
|
-
|
|
31
|
-
describe('validation before build, deploy and start', () => {
|
|
32
|
-
beforeEach(() => {
|
|
33
|
-
jest.clearAllMocks();
|
|
34
|
-
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
35
|
-
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
36
|
-
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
37
|
-
jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
afterEach(() => jest.restoreAllMocks());
|
|
41
|
-
|
|
42
|
-
it('build validates first and fails on errors', async () => {
|
|
43
|
-
preflightValidation.mockReturnValue(false);
|
|
44
|
-
|
|
45
|
-
await buildCommand({ stage: 'prod' });
|
|
46
|
-
|
|
47
|
-
expect(preflightValidation).toHaveBeenCalledWith({
|
|
48
|
-
command: 'build',
|
|
49
|
-
options: { stage: 'prod' },
|
|
50
|
-
failOnErrors: true,
|
|
51
|
-
});
|
|
52
|
-
expect(childProcess.spawnSync).not.toHaveBeenCalled();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('build packages after a passing validation', async () => {
|
|
56
|
-
preflightValidation.mockReturnValue(true);
|
|
57
|
-
|
|
58
|
-
await buildCommand({ stage: 'prod' });
|
|
59
|
-
|
|
60
|
-
expect(childProcess.spawnSync).toHaveBeenCalled();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('deploy validates first and fails on errors', async () => {
|
|
64
|
-
preflightValidation.mockReturnValue(false);
|
|
65
|
-
|
|
66
|
-
await deployCommand({ stage: 'prod', skipValidate: false });
|
|
67
|
-
|
|
68
|
-
expect(preflightValidation).toHaveBeenCalledWith({
|
|
69
|
-
command: 'deploy',
|
|
70
|
-
options: { stage: 'prod', skipValidate: false },
|
|
71
|
-
failOnErrors: true,
|
|
72
|
-
});
|
|
73
|
-
expect(childProcess.spawn).not.toHaveBeenCalled();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('start validates and only warns', async () => {
|
|
77
|
-
preflightValidation.mockReturnValue(true);
|
|
78
|
-
nodeChildProcess.spawn.mockReturnValue({ on: jest.fn() });
|
|
79
|
-
const previous = process.env.DATABASE_URL;
|
|
80
|
-
process.env.DATABASE_URL = 'postgresql://localhost/frigg';
|
|
81
|
-
|
|
82
|
-
await startCommand({ stage: 'dev' });
|
|
83
|
-
|
|
84
|
-
process.env.DATABASE_URL = previous;
|
|
85
|
-
expect(preflightValidation).toHaveBeenCalledWith({
|
|
86
|
-
command: 'start',
|
|
87
|
-
options: { stage: 'dev' },
|
|
88
|
-
failOnErrors: false,
|
|
89
|
-
});
|
|
90
|
-
expect(nodeChildProcess.spawn).toHaveBeenCalled();
|
|
91
|
-
});
|
|
92
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `frigg init` validates the generated app with the same code path as
|
|
3
|
-
* `frigg validate`, and never fails because of it (ADR-051).
|
|
4
|
-
*/
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
|
|
9
|
-
jest.mock('@inquirer/prompts', () => ({}));
|
|
10
|
-
|
|
11
|
-
const BackendFirstHandler = require('../../init-command/backend-first-handler');
|
|
12
|
-
|
|
13
|
-
function writeApp(source) {
|
|
14
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'frigg-init-'));
|
|
15
|
-
fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"backend"}');
|
|
16
|
-
fs.writeFileSync(path.join(dir, 'index.js'), source);
|
|
17
|
-
return dir;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe('frigg init validation', () => {
|
|
21
|
-
const dirs = [];
|
|
22
|
-
let log;
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
log = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
jest.restoreAllMocks();
|
|
30
|
-
for (const dir of dirs.splice(0))
|
|
31
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('validates the real generated definition, not a stand-in', async () => {
|
|
35
|
-
const dir = writeApp(
|
|
36
|
-
"module.exports = { Definition: { integrations: [], database: { postgres: { enable: true, management: 'create-new' } } } };"
|
|
37
|
-
);
|
|
38
|
-
dirs.push(dir);
|
|
39
|
-
const handler = new BackendFirstHandler(dir, {});
|
|
40
|
-
|
|
41
|
-
const ok = await handler.validateGeneratedAppDefinition(
|
|
42
|
-
path.join(dir, 'index.js')
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
expect(ok).toBe(false);
|
|
46
|
-
expect(log.mock.calls.flat().join('\n')).toContain(
|
|
47
|
-
'/database/postgres/management'
|
|
48
|
-
);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('stays quiet before npm install, when index.js cannot load its dependencies', async () => {
|
|
52
|
-
const dir = writeApp(
|
|
53
|
-
"require('@friggframework/api-module-does-not-exist');"
|
|
54
|
-
);
|
|
55
|
-
dirs.push(dir);
|
|
56
|
-
const handler = new BackendFirstHandler(dir, {});
|
|
57
|
-
|
|
58
|
-
await expect(
|
|
59
|
-
handler.validateGeneratedAppDefinition(path.join(dir, 'index.js'))
|
|
60
|
-
).resolves.toBe(true);
|
|
61
|
-
expect(log).not.toHaveBeenCalled();
|
|
62
|
-
});
|
|
63
|
-
});
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
const { checkIntegrations } = require('../checks/integrations');
|
|
2
|
-
|
|
3
|
-
class FakeApi {
|
|
4
|
-
static requesterType = 'oauth2';
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const moduleDefinition = () => ({
|
|
8
|
-
moduleName: 'acme',
|
|
9
|
-
API: FakeApi,
|
|
10
|
-
requiredAuthMethods: {
|
|
11
|
-
getToken: async () => {},
|
|
12
|
-
getEntityDetails: async () => {},
|
|
13
|
-
getCredentialDetails: async () => {},
|
|
14
|
-
testAuthRequest: async () => {},
|
|
15
|
-
apiPropertiesToPersist: { credential: [], entity: [] },
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
function integrationClass(definition) {
|
|
20
|
-
return class Integration {
|
|
21
|
-
static Definition = definition;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const goodIntegration = () =>
|
|
26
|
-
integrationClass({
|
|
27
|
-
name: 'acme',
|
|
28
|
-
version: '1.0.0',
|
|
29
|
-
modules: { acme: { definition: moduleDefinition() } },
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const codes = (issues) =>
|
|
33
|
-
issues.map((i) => `${i.severity}:${i.code}:${i.pointer}`);
|
|
34
|
-
|
|
35
|
-
describe('checkIntegrations', () => {
|
|
36
|
-
it('accepts a well-formed integration class', () => {
|
|
37
|
-
expect(
|
|
38
|
-
checkIntegrations({ integrations: [goodIntegration()] })
|
|
39
|
-
).toEqual([]);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('warns when the app lists no integrations', () => {
|
|
43
|
-
expect(codes(checkIntegrations({ integrations: [] }))).toEqual([
|
|
44
|
-
'warning:no-integrations:/integrations',
|
|
45
|
-
]);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('rejects an entry that is not a class', () => {
|
|
49
|
-
expect(
|
|
50
|
-
codes(
|
|
51
|
-
checkIntegrations({
|
|
52
|
-
integrations: [{ Definition: { name: 'x' } }],
|
|
53
|
-
})
|
|
54
|
-
)
|
|
55
|
-
).toEqual(['error:integration-not-a-class:/integrations/0']);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('rejects a class without a static Definition', () => {
|
|
59
|
-
class Bare {}
|
|
60
|
-
expect(codes(checkIntegrations({ integrations: [Bare] }))).toEqual([
|
|
61
|
-
'error:integration-definition-missing:/integrations/0/Definition',
|
|
62
|
-
]);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('requires a valid name and modules, and warns on a missing version', () => {
|
|
66
|
-
const Cls = integrationClass({ name: '1 bad name' });
|
|
67
|
-
expect(codes(checkIntegrations({ integrations: [Cls] }))).toEqual([
|
|
68
|
-
'error:integration-name-invalid:/integrations/0/Definition/name',
|
|
69
|
-
'warning:integration-version-missing:/integrations/0/Definition/version',
|
|
70
|
-
'error:integration-modules-missing:/integrations/0/Definition/modules',
|
|
71
|
-
]);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('reports duplicate integration names', () => {
|
|
75
|
-
expect(
|
|
76
|
-
codes(
|
|
77
|
-
checkIntegrations({
|
|
78
|
-
integrations: [goodIntegration(), goodIntegration()],
|
|
79
|
-
})
|
|
80
|
-
)
|
|
81
|
-
).toEqual([
|
|
82
|
-
'error:integration-name-duplicate:/integrations/1/Definition/name',
|
|
83
|
-
]);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('checks module definitions the way Module.validateDefinition does', () => {
|
|
87
|
-
const definition = moduleDefinition();
|
|
88
|
-
delete definition.requiredAuthMethods.getToken;
|
|
89
|
-
delete definition.moduleName;
|
|
90
|
-
const Cls = integrationClass({
|
|
91
|
-
name: 'acme',
|
|
92
|
-
version: '1',
|
|
93
|
-
modules: { acme: { definition }, other: {} },
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(codes(checkIntegrations({ integrations: [Cls] }))).toEqual([
|
|
97
|
-
'error:module-name-missing:/integrations/0/Definition/modules/acme/definition/moduleName',
|
|
98
|
-
'error:module-auth-method-missing:/integrations/0/Definition/modules/acme/definition/requiredAuthMethods/getToken',
|
|
99
|
-
'error:module-definition-missing:/integrations/0/Definition/modules/other/definition',
|
|
100
|
-
]);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('does not require getToken for non-OAuth2 modules', () => {
|
|
104
|
-
const definition = moduleDefinition();
|
|
105
|
-
definition.API = class {
|
|
106
|
-
static requesterType = 'apiKey';
|
|
107
|
-
};
|
|
108
|
-
delete definition.requiredAuthMethods.getToken;
|
|
109
|
-
const Cls = integrationClass({
|
|
110
|
-
name: 'acme',
|
|
111
|
-
version: '1',
|
|
112
|
-
modules: { acme: { definition } },
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
expect(checkIntegrations({ integrations: [Cls] })).toEqual([]);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('warns that a module rateLimit is not applied', () => {
|
|
119
|
-
const definition = {
|
|
120
|
-
...moduleDefinition(),
|
|
121
|
-
rateLimit: { requests: 10, period: 'second' },
|
|
122
|
-
};
|
|
123
|
-
const Cls = integrationClass({
|
|
124
|
-
name: 'acme',
|
|
125
|
-
version: '1',
|
|
126
|
-
modules: { acme: { definition } },
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
expect(codes(checkIntegrations({ integrations: [Cls] }))).toEqual([
|
|
130
|
-
'warning:module-rate-limit-ignored:/integrations/0/Definition/modules/acme/definition/rateLimit',
|
|
131
|
-
]);
|
|
132
|
-
});
|
|
133
|
-
});
|