@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.
Files changed (29) hide show
  1. package/frigg-cli/README.md +1 -38
  2. package/frigg-cli/__tests__/unit/commands/build.test.js +0 -3
  3. package/frigg-cli/__tests__/unit/commands/deploy.test.js +0 -3
  4. package/frigg-cli/build-command/index.js +0 -6
  5. package/frigg-cli/deploy-command/index.js +0 -6
  6. package/frigg-cli/index.js +1 -13
  7. package/frigg-cli/init-command/backend-first-handler.js +46 -25
  8. package/frigg-cli/start-command/index.js +0 -4
  9. package/frigg-cli/start-command/start-command.test.js +0 -3
  10. package/infrastructure/__tests__/management-api-gateway-routes.test.js +281 -0
  11. package/infrastructure/create-frigg-infrastructure.js +1 -4
  12. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +98 -60
  13. package/infrastructure/domains/shared/utilities/base-definition-factory.js +19 -0
  14. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +13 -1
  15. package/package.json +7 -7
  16. package/frigg-cli/__tests__/unit/commands/validate-wiring.test.js +0 -92
  17. package/frigg-cli/validate-command/__tests__/init-validation.test.js +0 -63
  18. package/frigg-cli/validate-command/__tests__/integrations-checks.test.js +0 -133
  19. package/frigg-cli/validate-command/__tests__/validate-app-definition.test.js +0 -326
  20. package/frigg-cli/validate-command/__tests__/validate-command.test.js +0 -241
  21. package/frigg-cli/validate-command/checks/config.js +0 -468
  22. package/frigg-cli/validate-command/checks/integrations.js +0 -274
  23. package/frigg-cli/validate-command/format-report.js +0 -58
  24. package/frigg-cli/validate-command/index.js +0 -107
  25. package/frigg-cli/validate-command/load-app-definition.js +0 -93
  26. package/frigg-cli/validate-command/schema-issues.js +0 -154
  27. package/frigg-cli/validate-command/validate-app-definition.js +0 -60
  28. package/infrastructure/app-definition-schema-contract.test.js +0 -607
  29. package/infrastructure/create-frigg-infrastructure.test.js +0 -51
@@ -1,326 +0,0 @@
1
- const { validateDefinition } = require('../validate-app-definition');
2
-
3
- class Api {
4
- static requesterType = 'oauth2';
5
- }
6
- const auth = {
7
- getToken: async () => {},
8
- getEntityDetails: async () => {},
9
- getCredentialDetails: async () => {},
10
- testAuthRequest: async () => {},
11
- apiPropertiesToPersist: { credential: [], entity: [] },
12
- };
13
- class AcmeIntegration {
14
- static Definition = {
15
- name: 'acme',
16
- version: '1.0.0',
17
- modules: {
18
- acme: {
19
- definition: {
20
- moduleName: 'acme',
21
- API: Api,
22
- requiredAuthMethods: auth,
23
- },
24
- },
25
- },
26
- };
27
- }
28
-
29
- /** The app `frigg init` scaffolds, with one integration installed. */
30
- const scaffold = (overrides = {}) => ({
31
- name: 'frigg-app',
32
- integrations: [AcmeIntegration],
33
- user: {
34
- usePassword: true,
35
- primary: 'individual',
36
- individualUserRequired: true,
37
- organizationUserRequired: false,
38
- },
39
- encryption: { fieldLevelEncryptionMethod: 'kms' },
40
- managementMode: 'managed',
41
- vpcIsolation: 'isolated',
42
- vpc: { enable: false },
43
- database: {
44
- postgres: {
45
- enable: true,
46
- management: 'use-existing',
47
- endpoint: 'db.example.com',
48
- },
49
- },
50
- environment: { DATABASE_URL: true },
51
- logging: { retentionInDays: 14 },
52
- ssm: { enable: false },
53
- ...overrides,
54
- });
55
-
56
- const summary = ({ errors, warnings }) => ({
57
- errors: errors.map((i) => `${i.code} ${i.pointer}`),
58
- warnings: warnings.map((i) => `${i.code} ${i.pointer}`),
59
- });
60
-
61
- describe('validateDefinition', () => {
62
- it('passes a scaffolded app with an integration class', () => {
63
- expect(
64
- summary(
65
- validateDefinition({
66
- definition: scaffold(),
67
- stage: 'prod',
68
- env: {},
69
- })
70
- )
71
- ).toEqual({
72
- errors: [],
73
- warnings: ['no-effect /database/postgres/management'],
74
- });
75
- });
76
-
77
- it('reports schema errors with JSON pointers and fix hints', () => {
78
- const result = validateDefinition({
79
- definition: scaffold({
80
- vpc: { enable: false, mangement: 'discover' },
81
- logging: { level: 'loud' },
82
- }),
83
- env: {},
84
- });
85
-
86
- expect(result.errors).toEqual([
87
- expect.objectContaining({
88
- code: 'unknown-key',
89
- pointer: '/vpc/mangement',
90
- hint: 'Did you mean "management"?',
91
- }),
92
- expect.objectContaining({
93
- code: 'invalid-value',
94
- pointer: '/logging/level',
95
- hint: expect.stringContaining('"debug"'),
96
- }),
97
- ]);
98
- });
99
-
100
- it("rejects database.postgres.management 'existing', which the Aurora builder rejects", () => {
101
- const definition = scaffold({
102
- managementMode: undefined,
103
- database: { postgres: { enable: true, management: 'existing' } },
104
- vpc: { enable: true },
105
- });
106
-
107
- expect(
108
- summary(validateDefinition({ definition, env: {} })).errors
109
- ).toEqual(['invalid-value /database/postgres/management']);
110
- });
111
-
112
- it('reports a broken integration class', () => {
113
- class Broken {
114
- static Definition = { name: 'broken' };
115
- }
116
- const result = validateDefinition({
117
- definition: scaffold({ integrations: [Broken] }),
118
- env: {},
119
- });
120
-
121
- expect(summary(result).errors).toEqual([
122
- 'integration-modules-missing /integrations/0/Definition/modules',
123
- ]);
124
- });
125
-
126
- it('reports an integration that is not a class once', () => {
127
- const result = validateDefinition({
128
- definition: scaffold({ integrations: ['acme'] }),
129
- env: {},
130
- });
131
-
132
- expect(summary(result).errors).toEqual([
133
- 'integration-not-a-class /integrations/0',
134
- ]);
135
- });
136
-
137
- it('reports a missing database', () => {
138
- const result = validateDefinition({
139
- definition: scaffold({ database: undefined }),
140
- env: {},
141
- });
142
-
143
- expect(summary(result).errors).toEqual(['no-database /database']);
144
- });
145
-
146
- it('a non-object definition is a schema error, not a crash', () => {
147
- expect(
148
- summary(validateDefinition({ definition: 'nope', env: {} })).errors
149
- ).toEqual(['invalid-type ']);
150
- });
151
- });
152
-
153
- describe('semantic checks', () => {
154
- const run = (overrides, options = {}) =>
155
- summary(
156
- validateDefinition({
157
- definition: scaffold(overrides),
158
- env: {},
159
- ...options,
160
- })
161
- );
162
-
163
- describe('encryption', () => {
164
- it("requires AES_KEY_ID and AES_KEY in environment for 'aes'", () => {
165
- expect(
166
- run({ encryption: { fieldLevelEncryptionMethod: 'aes' } })
167
- .errors
168
- ).toEqual([
169
- 'missing-encryption-env /environment/AES_KEY_ID',
170
- 'missing-encryption-env /environment/AES_KEY',
171
- ]);
172
- });
173
-
174
- it('warns when the AES values are missing from the deploying shell', () => {
175
- const result = run(
176
- {
177
- encryption: { fieldLevelEncryptionMethod: 'aes' },
178
- environment: {
179
- DATABASE_URL: true,
180
- AES_KEY_ID: true,
181
- AES_KEY: true,
182
- },
183
- },
184
- { stage: 'prod', env: { AES_KEY_ID: 'k1' } }
185
- );
186
- expect(result.errors).toEqual([]);
187
- expect(result.warnings).toContain(
188
- 'missing-env-value /environment/AES_KEY'
189
- );
190
- });
191
-
192
- it('warns that a deployed stage without a method stores plaintext', () => {
193
- expect(
194
- run({ encryption: undefined }, { stage: 'prod' }).warnings
195
- ).toContain(
196
- 'no-field-encryption /encryption/fieldLevelEncryptionMethod'
197
- );
198
- });
199
-
200
- it('warns that stage dev never encrypts', () => {
201
- expect(run({}, { stage: 'dev' }).warnings).toContain(
202
- 'encryption-bypassed /encryption/fieldLevelEncryptionMethod'
203
- );
204
- });
205
-
206
- it('warns about the KMS key fallback outside managed mode', () => {
207
- expect(
208
- run({ managementMode: undefined, vpcIsolation: undefined })
209
- .warnings
210
- ).toContain('kms-key-source /encryption/createResourceIfNoneFound');
211
- });
212
- });
213
-
214
- describe('user', () => {
215
- it('rejects a user config with which login always fails', () => {
216
- expect(
217
- run({ user: { individualUserRequired: false } }).errors
218
- ).toEqual(['no-login /user']);
219
- });
220
-
221
- it('warns an organization-only app that individualUserRequired now defaults to true', () => {
222
- expect(
223
- run({ user: { organizationUserRequired: true } }).warnings
224
- ).toContain('default-changed /user/individualUserRequired');
225
- });
226
-
227
- it('rejects adopterJwt without a secret and an app with every auth mode off', () => {
228
- expect(
229
- run({
230
- user: {
231
- authModes: {
232
- friggToken: false,
233
- sharedSecret: false,
234
- adopterJwt: false,
235
- },
236
- },
237
- }).errors
238
- ).toEqual(['no-auth-mode /user/authModes']);
239
- expect(
240
- run({ user: { authModes: { adopterJwt: true } } }).errors
241
- ).toEqual(['missing-key /user/jwtConfig/secret']);
242
- });
243
-
244
- it('warns that user.password is the old spelling of usePassword', () => {
245
- expect(run({ user: { password: true } }).warnings).toContain(
246
- 'deprecated-alias /user/password'
247
- );
248
- });
249
- });
250
-
251
- describe('management and VPC', () => {
252
- it("warns that 'managed' does not stop the VPC builder applying vpc.management", () => {
253
- expect(
254
- run({ vpc: { enable: true, management: 'discover' } }).warnings
255
- ).toContain('conflicting-settings /vpc/management');
256
- });
257
-
258
- it("warns when 'managed' is used without vpcIsolation", () => {
259
- expect(run({ vpcIsolation: undefined }).warnings).toContain(
260
- 'ambiguous-default /vpcIsolation'
261
- );
262
- });
263
-
264
- it('warns about subnet and NAT values the VPC builder ignores', () => {
265
- const result = run({
266
- managementMode: undefined,
267
- vpc: {
268
- enable: true,
269
- subnets: { management: 'create-new' },
270
- natGateway: { management: 'discover' },
271
- },
272
- });
273
- expect(result.warnings).toEqual(
274
- expect.arrayContaining([
275
- 'no-effect /vpc/subnets/management',
276
- 'no-effect /vpc/natGateway/management',
277
- ])
278
- );
279
- });
280
-
281
- it('requires the ids use-existing needs', () => {
282
- const result = run({
283
- managementMode: undefined,
284
- vpc: {
285
- enable: true,
286
- management: 'use-existing',
287
- subnets: { management: 'use-existing' },
288
- },
289
- database: {
290
- postgres: { enable: true, management: 'use-existing' },
291
- },
292
- });
293
- expect(result.errors).toEqual([
294
- 'missing-key /vpc/vpcId',
295
- 'missing-key /vpc/subnets/ids',
296
- 'missing-key /database/postgres/endpoint',
297
- ]);
298
- });
299
-
300
- it('warns that a private Aurora cluster is unreachable from Lambdas outside a VPC', () => {
301
- expect(
302
- run({
303
- managementMode: undefined,
304
- database: {
305
- postgres: { enable: true, management: 'managed' },
306
- },
307
- }).warnings
308
- ).toContain('unreachable-database /vpc/enable');
309
- });
310
- });
311
-
312
- it('warns about keys nothing reads', () => {
313
- expect(
314
- run({ custom: { appName: 'x' }, security: { cors: {} } }).warnings
315
- ).toEqual(
316
- expect.arrayContaining(['no-effect /security', 'no-effect /custom'])
317
- );
318
- });
319
-
320
- it("warns that 'ssm' environment entries need ssm.enable", () => {
321
- expect(
322
- run({ environment: { DATABASE_URL: true, API_KEY: 'ssm' } })
323
- .warnings
324
- ).toContain('ssm-inactive /environment/API_KEY');
325
- });
326
- });
@@ -1,241 +0,0 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
-
5
- const { validateCommand, runValidation, preflightValidation } = require('..');
6
-
7
- const GOOD_INDEX = `
8
- class Api { static requesterType = 'apiKey'; }
9
- const definition = {
10
- moduleName: 'acme',
11
- API: Api,
12
- requiredAuthMethods: {
13
- getEntityDetails: async () => {},
14
- getCredentialDetails: async () => {},
15
- testAuthRequest: async () => {},
16
- apiPropertiesToPersist: { credential: ['api_key'], entity: [] },
17
- },
18
- };
19
- class AcmeIntegration {
20
- static Definition = { name: 'acme', version: '1.0.0', modules: { acme: { definition } } };
21
- }
22
- module.exports = {
23
- Definition: {
24
- name: 'frigg-app',
25
- integrations: [AcmeIntegration],
26
- user: { usePassword: true },
27
- encryption: { fieldLevelEncryptionMethod: 'kms', createResourceIfNoneFound: true },
28
- database: { postgres: { enable: true, management: 'use-existing', endpoint: 'db.example.com' } },
29
- environment: { DATABASE_URL: true },
30
- },
31
- };
32
- `;
33
-
34
- const BAD_INDEX = `
35
- module.exports = {
36
- Definition: {
37
- integrations: [{ name: 'not-a-class' }],
38
- database: { postgres: { enable: true, management: 'create-new' } },
39
- encrpytion: { fieldLevelEncryptionMethod: 'kms' },
40
- },
41
- };
42
- `;
43
-
44
- function backend(indexSource) {
45
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'frigg-validate-'));
46
- fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"backend"}');
47
- fs.writeFileSync(path.join(dir, 'index.js'), indexSource);
48
- return dir;
49
- }
50
-
51
- describe('frigg validate', () => {
52
- const dirs = [];
53
- let log;
54
- let warn;
55
- let error;
56
- let exit;
57
-
58
- beforeEach(() => {
59
- log = jest.spyOn(console, 'log').mockImplementation(() => {});
60
- warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
61
- error = jest.spyOn(console, 'error').mockImplementation(() => {});
62
- exit = jest.spyOn(process, 'exit').mockImplementation(() => {});
63
- process.exitCode = undefined;
64
- });
65
-
66
- afterEach(() => {
67
- jest.restoreAllMocks();
68
- process.exitCode = undefined;
69
- for (const dir of dirs.splice(0))
70
- fs.rmSync(dir, { recursive: true, force: true });
71
- });
72
-
73
- const inBackend = (source) => {
74
- const dir = backend(source);
75
- dirs.push(dir);
76
- jest.spyOn(process, 'cwd').mockReturnValue(dir);
77
- return dir;
78
- };
79
-
80
- it('passes an app with integration classes and exits 0', async () => {
81
- inBackend(GOOD_INDEX);
82
-
83
- const report = await validateCommand({ stage: 'prod' });
84
-
85
- expect(report.errors).toEqual([]);
86
- expect(process.exitCode).toBeUndefined();
87
- expect(log.mock.calls.flat().join('\n')).toContain(
88
- 'The app definition is valid'
89
- );
90
- });
91
-
92
- it('prints errors with pointers and hints and exits 1', async () => {
93
- inBackend(BAD_INDEX);
94
-
95
- const report = await validateCommand({});
96
- const output = log.mock.calls.flat().join('\n');
97
-
98
- expect(process.exitCode).toBe(1);
99
- expect(report.errors.map((e) => e.pointer)).toEqual(
100
- expect.arrayContaining([
101
- '/encrpytion',
102
- '/database/postgres/management',
103
- '/integrations/0',
104
- ])
105
- );
106
- expect(output).toContain('/encrpytion');
107
- expect(output).toContain('Did you mean "encryption"?');
108
- expect(output).toContain(
109
- 'Use one of: "discover", "managed", "use-existing"'
110
- );
111
- });
112
-
113
- it('--json prints a machine-readable report', async () => {
114
- inBackend(BAD_INDEX);
115
-
116
- await validateCommand({ json: true, stage: 'prod' });
117
- const json = JSON.parse(log.mock.calls[0][0]);
118
-
119
- expect(json).toEqual(
120
- expect.objectContaining({
121
- valid: false,
122
- stage: 'prod',
123
- errors: expect.arrayContaining([
124
- expect.objectContaining({
125
- pointer: '/encrpytion',
126
- code: 'unknown-key',
127
- severity: 'error',
128
- }),
129
- ]),
130
- warnings: expect.any(Array),
131
- })
132
- );
133
- });
134
-
135
- it('reports a backend that fails to load', () => {
136
- const dir = backend("require('./missing-file');");
137
- dirs.push(dir);
138
-
139
- const report = runValidation({ cwd: dir });
140
-
141
- expect(report.errors).toEqual([
142
- expect.objectContaining({
143
- code: 'load-failed',
144
- message: expect.stringContaining('missing-file'),
145
- }),
146
- ]);
147
- });
148
-
149
- it('reports a directory without a backend', () => {
150
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'frigg-empty-'));
151
- dirs.push(dir);
152
-
153
- expect(runValidation({ cwd: dir }).errors).toEqual([
154
- expect.objectContaining({ code: 'backend-not-found' }),
155
- ]);
156
- });
157
-
158
- it('finds the backend/ directory from the project root', () => {
159
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'frigg-root-'));
160
- dirs.push(root);
161
- fs.mkdirSync(path.join(root, 'backend'));
162
- fs.writeFileSync(path.join(root, 'backend', 'package.json'), '{}');
163
- fs.writeFileSync(path.join(root, 'backend', 'index.js'), GOOD_INDEX);
164
-
165
- expect(runValidation({ cwd: root }).source).toBe(
166
- path.join(root, 'backend', 'index.js')
167
- );
168
- });
169
-
170
- it('reports an index.js without a Definition', () => {
171
- const dir = backend('module.exports = {};');
172
- dirs.push(dir);
173
-
174
- expect(runValidation({ cwd: dir }).errors).toEqual([
175
- expect.objectContaining({ code: 'definition-not-exported' }),
176
- ]);
177
- });
178
-
179
- describe('preflightValidation', () => {
180
- it('stops build/deploy on errors', () => {
181
- inBackend(BAD_INDEX);
182
-
183
- const proceed = preflightValidation({
184
- command: 'deploy',
185
- options: { stage: 'prod' },
186
- failOnErrors: true,
187
- });
188
-
189
- expect(proceed).toBe(false);
190
- expect(exit).toHaveBeenCalledWith(1);
191
- expect(error.mock.calls.flat().join('\n')).toContain(
192
- '--skip-validate'
193
- );
194
- });
195
-
196
- it('lets build/deploy continue on a valid definition', () => {
197
- inBackend(GOOD_INDEX);
198
-
199
- expect(
200
- preflightValidation({
201
- command: 'build',
202
- options: { stage: 'prod' },
203
- failOnErrors: true,
204
- })
205
- ).toBe(true);
206
- expect(exit).not.toHaveBeenCalled();
207
- });
208
-
209
- it('only warns for start', () => {
210
- inBackend(BAD_INDEX);
211
-
212
- expect(
213
- preflightValidation({
214
- command: 'start',
215
- options: {},
216
- failOnErrors: false,
217
- })
218
- ).toBe(true);
219
- expect(exit).not.toHaveBeenCalled();
220
- const output = log.mock.calls.flat().join('\n');
221
- expect(output).toContain('warning');
222
- expect(output).not.toMatch(/^\s+error /m);
223
- });
224
-
225
- it('--skip-validate skips validation, loudly', () => {
226
- inBackend(BAD_INDEX);
227
-
228
- expect(
229
- preflightValidation({
230
- command: 'deploy',
231
- options: { skipValidate: true },
232
- failOnErrors: true,
233
- })
234
- ).toBe(true);
235
- expect(exit).not.toHaveBeenCalled();
236
- expect(warn.mock.calls.flat().join('\n')).toContain(
237
- 'VALIDATION SKIPPED'
238
- );
239
- });
240
- });
241
- });