@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
package/frigg-cli/README.md
CHANGED
|
@@ -110,41 +110,6 @@ Found 5 modules matching "crm"
|
|
|
110
110
|
|
|
111
111
|
---
|
|
112
112
|
|
|
113
|
-
#### `frigg validate [options]`
|
|
114
|
-
|
|
115
|
-
Validate the app definition (`index.js`) before you build or deploy it (ADR-051).
|
|
116
|
-
|
|
117
|
-
**Usage:**
|
|
118
|
-
```bash
|
|
119
|
-
frigg validate # schema + runtime checks
|
|
120
|
-
frigg validate --stage prod # also the stage checks (encryption, deploy-time environment)
|
|
121
|
-
frigg validate --json # machine-readable report for CI
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**What it does:**
|
|
125
|
-
1. Loads `index.js` the way `frigg build` does (current directory, or the nearest `backend/`)
|
|
126
|
-
2. Validates it against the app-definition schema: unknown keys (with "did you mean"), types, and the values the infrastructure builders accept
|
|
127
|
-
3. Checks what the schema cannot: integration classes (static `Definition` with a valid, unique `name`, `version`, `modules`, and API module definitions that pass core's checks), an enabled database, management settings that conflict or lack ids, encryption (`aes` keys in `environment`, KMS key source, stages that never encrypt), a user config with which login works, `'ssm'` entries without `ssm.enable`, and keys that have no effect
|
|
128
|
-
4. Prints each problem with its JSON pointer and a fix; exits `1` when there are errors
|
|
129
|
-
|
|
130
|
-
**Example output:**
|
|
131
|
-
```
|
|
132
|
-
Validating /path/to/backend/index.js (stage: prod)
|
|
133
|
-
error /encrpytion Unknown key "encrpytion": nothing in Frigg reads it.
|
|
134
|
-
fix: Did you mean "encryption"?
|
|
135
|
-
error /database/postgres/management /database/postgres/management is "create-new", which is not an allowed value.
|
|
136
|
-
fix: Use one of: "discover", "managed", "use-existing".
|
|
137
|
-
warning /custom custom is not read by Frigg: it has no effect.
|
|
138
|
-
fix: Remove it.
|
|
139
|
-
✗ 2 error(s), 1 warning(s)
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
`--json` prints `{ valid, source, stage, errors, warnings }`; each issue is `{ severity, code, pointer, message, hint }`.
|
|
143
|
-
|
|
144
|
-
`frigg build` and `frigg deploy` run the same validation first and stop on errors (`--skip-validate` bypasses it, with a loud warning). `frigg start` runs it and only warns. `frigg init` validates the generated app the same way.
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
113
|
#### `frigg start [options]`
|
|
149
114
|
|
|
150
115
|
Start local development server with hot reload.
|
|
@@ -158,7 +123,7 @@ frigg start --frontend
|
|
|
158
123
|
```
|
|
159
124
|
|
|
160
125
|
**What it does:**
|
|
161
|
-
1. Validates DATABASE_URL environment variable
|
|
126
|
+
1. Validates DATABASE_URL environment variable
|
|
162
127
|
2. Detects database type (MongoDB or PostgreSQL)
|
|
163
128
|
3. Checks Prisma client generation status
|
|
164
129
|
4. Tests database connection
|
|
@@ -217,12 +182,10 @@ frigg deploy --region us-west-2
|
|
|
217
182
|
frigg deploy --force
|
|
218
183
|
frigg deploy --skip-env-validation
|
|
219
184
|
frigg deploy --skip-doctor # Skip health check (not recommended)
|
|
220
|
-
frigg deploy --skip-validate # Skip app definition validation (not recommended)
|
|
221
185
|
frigg deploy --no-interactive # CI/CD mode (no prompts, auto-repair safe issues)
|
|
222
186
|
```
|
|
223
187
|
|
|
224
188
|
**What it does:**
|
|
225
|
-
0. **Validates the app definition** (`frigg validate`) and stops on errors, unless `--skip-validate`
|
|
226
189
|
1. **Runs health check** (`frigg doctor`) to detect infrastructure issues
|
|
227
190
|
2. **Auto-repairs safe issues** (or prompts for confirmation in interactive mode)
|
|
228
191
|
3. **Fails if critical issues found** (unless `--skip-doctor` flag used)
|
|
@@ -14,9 +14,6 @@ jest.mock('child_process', () => ({
|
|
|
14
14
|
spawnSync: jest.fn()
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
|
-
// The app definition validation step is tested in validate-command/.
|
|
18
|
-
jest.mock('../../../validate-command', () => ({ preflightValidation: jest.fn(() => true) }));
|
|
19
|
-
|
|
20
17
|
// Require after mocks
|
|
21
18
|
const { spawnSync } = require('child_process');
|
|
22
19
|
const { buildCommand } = require('../../../build-command');
|
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
// Mock dependencies BEFORE requiring modules
|
|
13
|
-
// The app definition validation step is tested in validate-command/.
|
|
14
|
-
jest.mock('../../../validate-command', () => ({ preflightValidation: jest.fn(() => true) }));
|
|
15
|
-
|
|
16
13
|
jest.mock('child_process', () => ({
|
|
17
14
|
spawn: jest.fn()
|
|
18
15
|
}));
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
const { spawnSync } = require('child_process');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { preflightValidation } = require('../validate-command');
|
|
4
3
|
|
|
5
4
|
async function buildCommand(options) {
|
|
6
5
|
console.log('Building the serverless application...');
|
|
7
6
|
|
|
8
|
-
// Validate the app definition first (ADR-051); errors stop the build.
|
|
9
|
-
if (!preflightValidation({ command: 'build', options, failOnErrors: true })) {
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
7
|
// Suppress AWS SDK warning message about maintenance mode
|
|
14
8
|
process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE = '1';
|
|
15
9
|
|
|
@@ -4,7 +4,6 @@ const fs = require('fs');
|
|
|
4
4
|
|
|
5
5
|
// Import doctor command for post-deployment health check
|
|
6
6
|
const { doctorCommand } = require('../doctor-command');
|
|
7
|
-
const { preflightValidation } = require('../validate-command');
|
|
8
7
|
|
|
9
8
|
// Configuration constants
|
|
10
9
|
const PATHS = {
|
|
@@ -313,11 +312,6 @@ async function pushOffloadedParametersOrAbort(appDefinition, options) {
|
|
|
313
312
|
async function deployCommand(options) {
|
|
314
313
|
console.log('Deploying the serverless application...');
|
|
315
314
|
|
|
316
|
-
// Validate the app definition first (ADR-051); errors stop the deploy.
|
|
317
|
-
if (!preflightValidation({ command: 'deploy', options, failOnErrors: true })) {
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
315
|
const appDefinition = loadAppDefinition();
|
|
322
316
|
const environment = validateAndBuildEnvironment(appDefinition, options);
|
|
323
317
|
|
package/frigg-cli/index.js
CHANGED
|
@@ -87,7 +87,6 @@ const { doctorCommand } = require('./doctor-command');
|
|
|
87
87
|
const { repairCommand } = require('./repair-command');
|
|
88
88
|
const { authCommand } = require('./auth-command');
|
|
89
89
|
const { ssmPushCommand } = require('./ssm-command');
|
|
90
|
-
const { validateCommand } = require('./validate-command');
|
|
91
90
|
|
|
92
91
|
const program = new Command();
|
|
93
92
|
|
|
@@ -109,24 +108,14 @@ program
|
|
|
109
108
|
.description('Run the backend and optional frontend')
|
|
110
109
|
.option('-s, --stage <stage>', 'deployment stage', 'dev')
|
|
111
110
|
.option('-v, --verbose', 'enable verbose output')
|
|
112
|
-
.option('--skip-validate', 'skip app definition validation')
|
|
113
111
|
.action(startCommand);
|
|
114
112
|
|
|
115
|
-
program
|
|
116
|
-
.command('validate')
|
|
117
|
-
.description('Validate the app definition (index.js) against the schema and the runtime rules')
|
|
118
|
-
.option('-s, --stage <stage>', 'also run the checks for this stage (encryption, deploy-time environment)')
|
|
119
|
-
.option('--json', 'print the report as JSON')
|
|
120
|
-
.option('-v, --verbose', 'enable verbose output')
|
|
121
|
-
.action(validateCommand);
|
|
122
|
-
|
|
123
113
|
program
|
|
124
114
|
.command('build')
|
|
125
115
|
.description('Build the serverless application')
|
|
126
116
|
.option('-s, --stage <stage>', 'deployment stage', 'dev')
|
|
127
117
|
.option('-v, --verbose', 'enable verbose output')
|
|
128
118
|
.option('-p, --production', 'build for production (enables AWS discovery)')
|
|
129
|
-
.option('--skip-validate', 'skip app definition validation (not recommended)')
|
|
130
119
|
.action(buildCommand);
|
|
131
120
|
|
|
132
121
|
program
|
|
@@ -136,7 +125,6 @@ program
|
|
|
136
125
|
.option('-v, --verbose', 'enable verbose output')
|
|
137
126
|
.option('-f, --force', 'force deployment (bypasses caching for layers and functions)')
|
|
138
127
|
.option('--skip-doctor', 'skip post-deployment health check')
|
|
139
|
-
.option('--skip-validate', 'skip app definition validation (not recommended)')
|
|
140
128
|
.action(deployCommand);
|
|
141
129
|
|
|
142
130
|
program
|
|
@@ -232,4 +220,4 @@ authProgram
|
|
|
232
220
|
|
|
233
221
|
program.parse(process.argv);
|
|
234
222
|
|
|
235
|
-
module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand,
|
|
223
|
+
module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand, dbSetupCommand, doctorCommand, repairCommand, authCommand, ssmPushCommand };
|
|
@@ -5,8 +5,7 @@ const { select, confirm, multiselect } = require('@inquirer/prompts');
|
|
|
5
5
|
const { execSync } = require('child_process');
|
|
6
6
|
const spawn = require('cross-spawn');
|
|
7
7
|
const npmRegistry = require('../utils/npm-registry');
|
|
8
|
-
const {
|
|
9
|
-
const { formatReport } = require('../validate-command/format-report');
|
|
8
|
+
const { validateAppDefinition, formatErrors } = require('@friggframework/schemas');
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Backend-first template handler that treats frontend as optional demonstration
|
|
@@ -648,39 +647,61 @@ To integrate Frigg into your production application:
|
|
|
648
647
|
}
|
|
649
648
|
|
|
650
649
|
/**
|
|
651
|
-
* Validate
|
|
652
|
-
* `frigg validate`, `frigg build` and `frigg deploy` (ADR-051). Problems
|
|
653
|
-
* are reported as warnings: they never stop `frigg init`.
|
|
650
|
+
* Validate generated app definition against schema
|
|
654
651
|
*/
|
|
655
652
|
async validateGeneratedAppDefinition(appDefPath) {
|
|
656
653
|
try {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if (issues.length === 0) {
|
|
660
|
-
if (this.options.verbose) {
|
|
661
|
-
console.log(chalk.green('✅ App definition passes validation'));
|
|
662
|
-
}
|
|
663
|
-
return true;
|
|
654
|
+
if (this.options.verbose) {
|
|
655
|
+
console.log(chalk.gray('🔍 Validating app definition against schema...'));
|
|
664
656
|
}
|
|
665
|
-
|
|
666
|
-
|
|
657
|
+
|
|
658
|
+
// Read the generated index.js file
|
|
659
|
+
const content = await fs.readFile(appDefPath, 'utf8');
|
|
660
|
+
|
|
661
|
+
// Extract the appDefinition object (simplified approach)
|
|
662
|
+
// In a real scenario, we might use AST parsing for more robust extraction
|
|
663
|
+
const appDefinitionMatch = content.match(/const appDefinition = ({[\s\S]*?});/);
|
|
664
|
+
if (!appDefinitionMatch) {
|
|
665
|
+
throw new Error('Could not extract appDefinition from generated file');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Create a minimal representation for validation
|
|
669
|
+
// Since we can't easily execute the file, we'll validate the structure we know we generated
|
|
670
|
+
const appDefinition = {
|
|
671
|
+
integrations: [], // Will be populated based on selected integrations
|
|
672
|
+
user: { password: true },
|
|
673
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
674
|
+
vpc: { enable: true },
|
|
675
|
+
security: {
|
|
676
|
+
cors: {
|
|
677
|
+
origin: 'http://localhost:3000',
|
|
678
|
+
credentials: true
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
logging: { level: 'info', retentionInDays: 30 },
|
|
682
|
+
custom: {
|
|
683
|
+
appName: 'My Frigg Application',
|
|
684
|
+
version: '1.0.0',
|
|
685
|
+
environment: 'development'
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// Validate against schema
|
|
690
|
+
const result = validateAppDefinition(appDefinition);
|
|
691
|
+
|
|
692
|
+
if (result.valid) {
|
|
667
693
|
if (this.options.verbose) {
|
|
668
|
-
console.log(chalk.
|
|
694
|
+
console.log(chalk.green('✅ App definition passes schema validation'));
|
|
669
695
|
}
|
|
670
696
|
return true;
|
|
697
|
+
} else {
|
|
698
|
+
console.log(chalk.yellow('⚠️ App definition has validation warnings:'));
|
|
699
|
+
console.log(chalk.gray(formatErrors(result.errors)));
|
|
700
|
+
return false;
|
|
671
701
|
}
|
|
672
|
-
console.log(chalk.yellow('⚠️ App definition validation:'));
|
|
673
|
-
console.log(
|
|
674
|
-
formatReport({
|
|
675
|
-
...report,
|
|
676
|
-
errors: [],
|
|
677
|
-
warnings: issues.map((issue) => ({ ...issue, severity: 'warning' })),
|
|
678
|
-
})
|
|
679
|
-
);
|
|
680
|
-
return report.errors.length === 0;
|
|
681
702
|
} catch (error) {
|
|
682
703
|
if (this.options.verbose) {
|
|
683
|
-
console.log(chalk.yellow(`⚠️
|
|
704
|
+
console.log(chalk.yellow(`⚠️ Schema validation skipped: ${error.message}`));
|
|
684
705
|
}
|
|
685
706
|
return true; // Don't fail the process for validation issues
|
|
686
707
|
}
|
|
@@ -12,7 +12,6 @@ const {
|
|
|
12
12
|
getDatabaseTypeNotConfiguredError,
|
|
13
13
|
getPrismaClientNotGeneratedError
|
|
14
14
|
} = require('../utils/error-messages');
|
|
15
|
-
const { preflightValidation } = require('../validate-command');
|
|
16
15
|
|
|
17
16
|
async function startCommand(options) {
|
|
18
17
|
if (options.verbose) {
|
|
@@ -26,9 +25,6 @@ async function startCommand(options) {
|
|
|
26
25
|
const envPath = path.join(process.cwd(), '.env');
|
|
27
26
|
dotenv.config({ path: envPath });
|
|
28
27
|
|
|
29
|
-
// Validate the app definition (ADR-051); local development only warns.
|
|
30
|
-
preflightValidation({ command: 'start', options, failOnErrors: false });
|
|
31
|
-
|
|
32
28
|
// Pre-flight database checks
|
|
33
29
|
try {
|
|
34
30
|
await performDatabaseChecks(options.verbose);
|
|
@@ -23,9 +23,6 @@ jest.mock('node:child_process', () => ({
|
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
25
|
jest.mock('../utils/database-validator', () => mockValidator);
|
|
26
|
-
// The app definition validation step is tested in validate-command/.
|
|
27
|
-
jest.mock('../validate-command', () => ({ preflightValidation: jest.fn(() => true) }));
|
|
28
|
-
|
|
29
26
|
jest.mock('dotenv');
|
|
30
27
|
|
|
31
28
|
const { spawn } = require('node:child_process');
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Management API ↔ API Gateway route coverage
|
|
3
|
+
*
|
|
4
|
+
* The Management API is an Express app (core's createIntegrationRouter)
|
|
5
|
+
* served by the `auth` Lambda. API Gateway HTTP APIs have no implicit
|
|
6
|
+
* catch-all: a request whose path matches no declared route gets a gateway
|
|
7
|
+
* 404 and never reaches Express. So every route the integration router
|
|
8
|
+
* registers must be covered by an httpApi event on the `auth` function, and
|
|
9
|
+
* no more-specific route on another function may shadow it.
|
|
10
|
+
*
|
|
11
|
+
* The Express routes are read from the live router (not a hard-coded list),
|
|
12
|
+
* so a route added to core later fails this test until the gateway routes
|
|
13
|
+
* are updated alongside it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Avoid AWS calls: hand the builders a fixed discovery result.
|
|
17
|
+
jest.mock('../domains/shared/resource-discovery', () => {
|
|
18
|
+
const actual = jest.requireActual('../domains/shared/resource-discovery');
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
gatherDiscoveredResources: jest.fn().mockResolvedValue({
|
|
22
|
+
defaultVpcId: 'vpc-123456',
|
|
23
|
+
vpcCidr: '172.31.0.0/16',
|
|
24
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
25
|
+
privateSubnetId1: 'subnet-123456',
|
|
26
|
+
privateSubnetId2: 'subnet-789012',
|
|
27
|
+
publicSubnetId1: 'subnet-public-1',
|
|
28
|
+
publicSubnetId2: 'subnet-public-2',
|
|
29
|
+
defaultRouteTableId: 'rtb-123456',
|
|
30
|
+
defaultKmsKeyId:
|
|
31
|
+
'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012',
|
|
32
|
+
existingNatGatewayId: 'nat-default123',
|
|
33
|
+
auroraClusterEndpoint:
|
|
34
|
+
'test-cluster.cluster-abc123.us-east-1.rds.amazonaws.com',
|
|
35
|
+
auroraPort: 5432,
|
|
36
|
+
auroraEngine: 'aurora-postgresql',
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// createIntegrationRouter loads the app definition from the consuming
|
|
42
|
+
// backend; give it an empty one so the router can be built in isolation.
|
|
43
|
+
jest.mock('@friggframework/core/handlers/app-definition-loader', () => ({
|
|
44
|
+
loadAppDefinition: () => ({ integrations: [], userConfig: {} }),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const { composeServerlessDefinition } = require('../infrastructure-composer');
|
|
48
|
+
|
|
49
|
+
// Routes the auth Lambda's handler (core/handlers/routers/auth.js) adds on
|
|
50
|
+
// top of createIntegrationRouter().
|
|
51
|
+
const AUTH_HANDLER_EXTRA_ROUTES = [
|
|
52
|
+
{ method: 'GET', path: '/api/integrations/redirect/:appId' },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function getIntegrationRouterRoutes() {
|
|
56
|
+
const { createIntegrationRouter } = require('@friggframework/core');
|
|
57
|
+
// The repository factories pick an adapter from DB_TYPE (or the backend's
|
|
58
|
+
// app definition, which doesn't exist here). Nothing connects.
|
|
59
|
+
const previousDbType = process.env.DB_TYPE;
|
|
60
|
+
process.env.DB_TYPE = 'postgresql';
|
|
61
|
+
let app;
|
|
62
|
+
try {
|
|
63
|
+
app = createIntegrationRouter();
|
|
64
|
+
} finally {
|
|
65
|
+
if (previousDbType === undefined) delete process.env.DB_TYPE;
|
|
66
|
+
else process.env.DB_TYPE = previousDbType;
|
|
67
|
+
}
|
|
68
|
+
const stack = (app._router || app.router).stack;
|
|
69
|
+
const routes = [];
|
|
70
|
+
for (const layer of stack) {
|
|
71
|
+
if (!layer.route) continue;
|
|
72
|
+
for (const method of Object.keys(layer.route.methods)) {
|
|
73
|
+
routes.push({
|
|
74
|
+
method: method === '_all' ? 'ANY' : method.toUpperCase(),
|
|
75
|
+
path: layer.route.path,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return routes;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getGatewayRoutes(definition) {
|
|
83
|
+
const routes = [];
|
|
84
|
+
for (const [functionName, fn] of Object.entries(definition.functions)) {
|
|
85
|
+
for (const event of fn.events || []) {
|
|
86
|
+
if (!event.httpApi) continue;
|
|
87
|
+
routes.push({
|
|
88
|
+
functionName,
|
|
89
|
+
method: String(event.httpApi.method).toUpperCase(),
|
|
90
|
+
path: event.httpApi.path,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return routes;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const segmentsOf = (p) => p.split('/').filter(Boolean);
|
|
98
|
+
|
|
99
|
+
// Turn an Express path into a concrete request path, e.g.
|
|
100
|
+
// /api/entities/:entityId/options -> /api/entities/sample-entityId/options
|
|
101
|
+
const toConcretePath = (expressPath) =>
|
|
102
|
+
expressPath.replace(/:([A-Za-z0-9_]+)/g, 'sample-$1');
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* HTTP API path matching: literal segments match exactly, {param} matches
|
|
106
|
+
* one segment, a trailing {proxy+} greedily matches one or more segments.
|
|
107
|
+
* Returns null on no match, otherwise a specificity descriptor.
|
|
108
|
+
*/
|
|
109
|
+
function matchGatewayPath(gatewayPath, requestPath) {
|
|
110
|
+
const g = segmentsOf(gatewayPath);
|
|
111
|
+
const r = segmentsOf(requestPath);
|
|
112
|
+
let literals = 0;
|
|
113
|
+
for (let i = 0; i < g.length; i++) {
|
|
114
|
+
const seg = g[i];
|
|
115
|
+
if (/^\{[^}]+\+\}$/.test(seg)) {
|
|
116
|
+
if (i !== g.length - 1 || r.length <= i) return null;
|
|
117
|
+
return { greedy: true, literals, prefixLength: i };
|
|
118
|
+
}
|
|
119
|
+
if (i >= r.length) return null;
|
|
120
|
+
if (/^\{[^}]+\}$/.test(seg)) continue;
|
|
121
|
+
if (seg !== r[i]) return null;
|
|
122
|
+
literals++;
|
|
123
|
+
}
|
|
124
|
+
if (g.length !== r.length) return null;
|
|
125
|
+
return { greedy: false, literals, prefixLength: g.length };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Select the route API Gateway would invoke: a non-greedy match beats a
|
|
130
|
+
* greedy one, then more literal segments, then the longer greedy prefix,
|
|
131
|
+
* then an explicit method beats ANY.
|
|
132
|
+
*/
|
|
133
|
+
function selectGatewayRoute(gatewayRoutes, method, requestPath) {
|
|
134
|
+
const candidates = gatewayRoutes
|
|
135
|
+
.filter(
|
|
136
|
+
(r) => r.method === 'ANY' || method === 'ANY' || r.method === method
|
|
137
|
+
)
|
|
138
|
+
.map((r) => ({
|
|
139
|
+
route: r,
|
|
140
|
+
match: matchGatewayPath(r.path, requestPath),
|
|
141
|
+
}))
|
|
142
|
+
.filter((c) => c.match);
|
|
143
|
+
candidates.sort(
|
|
144
|
+
(a, b) =>
|
|
145
|
+
Number(a.match.greedy) - Number(b.match.greedy) ||
|
|
146
|
+
b.match.literals - a.match.literals ||
|
|
147
|
+
b.match.prefixLength - a.match.prefixLength ||
|
|
148
|
+
Number(a.route.method === 'ANY') - Number(b.route.method === 'ANY')
|
|
149
|
+
);
|
|
150
|
+
return candidates.length ? candidates[0].route : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
describe('Management API routes are reachable through API Gateway', () => {
|
|
154
|
+
let gatewayRoutes;
|
|
155
|
+
let managementRoutes;
|
|
156
|
+
const originalArgv = process.argv;
|
|
157
|
+
|
|
158
|
+
beforeAll(async () => {
|
|
159
|
+
process.argv = ['node', 'test'];
|
|
160
|
+
process.env.AWS_REGION = 'us-east-1';
|
|
161
|
+
// The builders narrate their progress; keep the test output quiet.
|
|
162
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
163
|
+
const warnSpy = jest
|
|
164
|
+
.spyOn(console, 'warn')
|
|
165
|
+
.mockImplementation(() => {});
|
|
166
|
+
try {
|
|
167
|
+
const definition = await composeServerlessDefinition({
|
|
168
|
+
name: 'route-coverage-app',
|
|
169
|
+
provider: 'aws',
|
|
170
|
+
usePrismaLambdaLayer: false,
|
|
171
|
+
database: { postgres: { enable: true } },
|
|
172
|
+
websockets: { enable: true },
|
|
173
|
+
adminScripts: [
|
|
174
|
+
class ExampleScript {
|
|
175
|
+
static Definition = { name: 'example-script' };
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
admin: { includeBuiltinReports: true },
|
|
179
|
+
integrations: [
|
|
180
|
+
{
|
|
181
|
+
Definition: {
|
|
182
|
+
name: 'acme',
|
|
183
|
+
webhooks: true,
|
|
184
|
+
extensions: {
|
|
185
|
+
inbound: {
|
|
186
|
+
extension: {
|
|
187
|
+
routes: [
|
|
188
|
+
{ path: '/events', method: 'POST' },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
// Integration names share the /api/ namespace with the
|
|
196
|
+
// Management API; make sure they don't shadow it.
|
|
197
|
+
{ Definition: { name: 'entities' } },
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
gatewayRoutes = getGatewayRoutes(definition);
|
|
201
|
+
} finally {
|
|
202
|
+
logSpy.mockRestore();
|
|
203
|
+
warnSpy.mockRestore();
|
|
204
|
+
}
|
|
205
|
+
managementRoutes = [
|
|
206
|
+
...getIntegrationRouterRoutes(),
|
|
207
|
+
...AUTH_HANDLER_EXTRA_ROUTES,
|
|
208
|
+
];
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
afterAll(() => {
|
|
212
|
+
process.argv = originalArgv;
|
|
213
|
+
delete process.env.AWS_REGION;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('reads the Management API routes from the core integration router', () => {
|
|
217
|
+
const paths = managementRoutes.map((r) => r.path);
|
|
218
|
+
expect(paths).toEqual(
|
|
219
|
+
expect.arrayContaining([
|
|
220
|
+
'/api/integrations',
|
|
221
|
+
'/api/authorize',
|
|
222
|
+
'/api/entity',
|
|
223
|
+
'/api/entity/options/:credentialId',
|
|
224
|
+
'/api/entities/:entityId',
|
|
225
|
+
])
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('routes every Management API route to the auth function', () => {
|
|
230
|
+
const unreachable = managementRoutes
|
|
231
|
+
.map(({ method, path: expressPath }) => {
|
|
232
|
+
const requestPath = toConcretePath(expressPath);
|
|
233
|
+
const selected = selectGatewayRoute(
|
|
234
|
+
gatewayRoutes,
|
|
235
|
+
method,
|
|
236
|
+
requestPath
|
|
237
|
+
);
|
|
238
|
+
return {
|
|
239
|
+
route: `${method} ${expressPath}`,
|
|
240
|
+
reachedBy: selected
|
|
241
|
+
? `${selected.method} ${selected.path} -> ${selected.functionName}`
|
|
242
|
+
: 'no API Gateway route',
|
|
243
|
+
ok: selected && selected.functionName === 'auth',
|
|
244
|
+
};
|
|
245
|
+
})
|
|
246
|
+
.filter((r) => !r.ok)
|
|
247
|
+
.map(({ route, reachedBy }) => `${route} (${reachedBy})`);
|
|
248
|
+
|
|
249
|
+
expect(unreachable).toEqual([]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it.each([
|
|
253
|
+
['GET', '/api/v2/reports', 'reportRouter'],
|
|
254
|
+
['POST', '/api/v2/reports/daily/run', 'reportRouter'],
|
|
255
|
+
['GET', '/admin/scripts/example-script', 'adminScriptRouter'],
|
|
256
|
+
['POST', '/api/acme-integration/webhooks', 'acmeWebhook'],
|
|
257
|
+
['POST', '/api/acme-integration/inbound/events', 'acme__inbound'],
|
|
258
|
+
['GET', '/api/acme-integration/custom', 'acme'],
|
|
259
|
+
['GET', '/api/entities-integration/custom', 'entities'],
|
|
260
|
+
['POST', '/user/login', 'user'],
|
|
261
|
+
['GET', '/health/ready', 'health'],
|
|
262
|
+
])('leaves %s %s on %s', (method, requestPath, functionName) => {
|
|
263
|
+
const selected = selectGatewayRoute(gatewayRoutes, method, requestPath);
|
|
264
|
+
expect(selected && selected.functionName).toBe(functionName);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('declares no method + path pair on more than one function', () => {
|
|
268
|
+
const seen = new Map();
|
|
269
|
+
const duplicates = [];
|
|
270
|
+
for (const r of gatewayRoutes) {
|
|
271
|
+
const key = `${r.method} ${r.path}`;
|
|
272
|
+
if (seen.has(key) && seen.get(key) !== r.functionName) {
|
|
273
|
+
duplicates.push(
|
|
274
|
+
`${key}: ${seen.get(key)} and ${r.functionName}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
seen.set(key, r.functionName);
|
|
278
|
+
}
|
|
279
|
+
expect(duplicates).toEqual([]);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -3,7 +3,6 @@ const fs = require('fs-extra');
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { composeServerlessDefinition } = require('./infrastructure-composer');
|
|
5
5
|
const { findNearestBackendPackageJson } = require('@friggframework/core');
|
|
6
|
-
const { applyAppDefinitionDefaults } = require('@friggframework/schemas');
|
|
7
6
|
|
|
8
7
|
// Filesystem-based cache to persist across osls require cache clears
|
|
9
8
|
const getCachePath = (backendDir) => {
|
|
@@ -103,9 +102,7 @@ async function createFriggInfrastructure() {
|
|
|
103
102
|
|
|
104
103
|
try {
|
|
105
104
|
const backend = require(backendFilePath);
|
|
106
|
-
|
|
107
|
-
// builders and the runtime see the same definition.
|
|
108
|
-
const appDefinition = applyAppDefinitionDefaults(backend.Definition);
|
|
105
|
+
const appDefinition = backend.Definition;
|
|
109
106
|
|
|
110
107
|
const definition = await composeServerlessDefinition(
|
|
111
108
|
appDefinition,
|