@friggframework/devtools 2.0.0-next.76 → 2.0.0-next.78

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.
@@ -15,9 +15,15 @@
15
15
  * and externally-provided SQS queues.
16
16
  */
17
17
 
18
- const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
18
+ const {
19
+ InfrastructureBuilder,
20
+ ValidationResult,
21
+ } = require('../shared/base-builder');
19
22
  const IntegrationResourceResolver = require('./integration-resolver');
20
- const { createEmptyDiscoveryResult, ResourceOwnership } = require('../shared/types');
23
+ const {
24
+ createEmptyDiscoveryResult,
25
+ ResourceOwnership,
26
+ } = require('../shared/types');
21
27
 
22
28
  class IntegrationBuilder extends InfrastructureBuilder {
23
29
  constructor() {
@@ -26,7 +32,10 @@ class IntegrationBuilder extends InfrastructureBuilder {
26
32
  }
27
33
 
28
34
  shouldExecute(appDefinition) {
29
- return Array.isArray(appDefinition.integrations) && appDefinition.integrations.length > 0;
35
+ return (
36
+ Array.isArray(appDefinition.integrations) &&
37
+ appDefinition.integrations.length > 0
38
+ );
30
39
  }
31
40
 
32
41
  getDependencies() {
@@ -48,7 +57,9 @@ class IntegrationBuilder extends InfrastructureBuilder {
48
57
  // Validate each integration
49
58
  appDefinition.integrations.forEach((integration, index) => {
50
59
  if (!integration?.Definition?.name) {
51
- result.addError(`Integration at index ${index} is missing Definition or name`);
60
+ result.addError(
61
+ `Integration at index ${index} is missing Definition or name`
62
+ );
52
63
  }
53
64
  });
54
65
 
@@ -60,7 +71,9 @@ class IntegrationBuilder extends InfrastructureBuilder {
60
71
  */
61
72
  async build(appDefinition, discoveredResources) {
62
73
  console.log(`\n[${this.name}] Configuring integrations...`);
63
- console.log(` Processing ${appDefinition.integrations.length} integrations...`);
74
+ console.log(
75
+ ` Processing ${appDefinition.integrations.length} integrations...`
76
+ );
64
77
 
65
78
  const usePrismaLayer = appDefinition.usePrismaLambdaLayer !== false;
66
79
 
@@ -73,23 +86,34 @@ class IntegrationBuilder extends InfrastructureBuilder {
73
86
  };
74
87
 
75
88
  // Get structured discovery result
76
- const discovery = discoveredResources._structured || this.convertFlatDiscoveryToStructured(discoveredResources);
89
+ const discovery =
90
+ discoveredResources._structured ||
91
+ this.convertFlatDiscoveryToStructured(discoveredResources);
77
92
 
78
93
  // Use IntegrationResourceResolver to make ownership decisions
79
94
  const resolver = new IntegrationResourceResolver();
80
95
  const decisions = resolver.resolveAll(appDefinition, discovery);
81
96
 
82
97
  console.log('\n 📋 Resource Ownership Decisions:');
83
- console.log(` InternalErrorQueue: ${decisions.internalErrorQueue.ownership} - ${decisions.internalErrorQueue.reason}`);
98
+ console.log(
99
+ ` InternalErrorQueue: ${decisions.internalErrorQueue.ownership} - ${decisions.internalErrorQueue.reason}`
100
+ );
84
101
 
85
102
  // Log per-integration decisions
86
- Object.keys(decisions.integrations).forEach(integrationName => {
103
+ Object.keys(decisions.integrations).forEach((integrationName) => {
87
104
  const queueDecision = decisions.integrations[integrationName].queue;
88
- console.log(` ${integrationName}Queue: ${queueDecision.ownership} - ${queueDecision.reason}`);
105
+ console.log(
106
+ ` ${integrationName}Queue: ${queueDecision.ownership} - ${queueDecision.reason}`
107
+ );
89
108
  });
90
109
 
91
110
  // Build resources based on ownership decisions
92
- await this.buildFromDecisions(decisions, appDefinition, result, usePrismaLayer);
111
+ await this.buildFromDecisions(
112
+ decisions,
113
+ appDefinition,
114
+ result,
115
+ usePrismaLayer
116
+ );
93
117
 
94
118
  console.log(`[${this.name}] ✅ Integration configuration completed`);
95
119
  return result;
@@ -113,7 +137,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
113
137
 
114
138
  // Add stack-managed resources from existingLogicalIds
115
139
  const existingLogicalIds = flatDiscovery.existingLogicalIds || [];
116
- existingLogicalIds.forEach(logicalId => {
140
+ existingLogicalIds.forEach((logicalId) => {
117
141
  let resourceType = '';
118
142
  let physicalId = '';
119
143
 
@@ -124,7 +148,9 @@ class IntegrationBuilder extends InfrastructureBuilder {
124
148
  } else if (logicalId.endsWith('Queue')) {
125
149
  // Integration-specific queue (e.g., SlackQueue, HubspotQueue)
126
150
  resourceType = 'AWS::SQS::Queue';
127
- const integrationName = logicalId.replace('Queue', '').toLowerCase();
151
+ const integrationName = logicalId
152
+ .replace('Queue', '')
153
+ .toLowerCase();
128
154
  physicalId = flatDiscovery[`${integrationName}QueueUrl`];
129
155
  }
130
156
 
@@ -132,7 +158,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
132
158
  discovery.stackManaged.push({
133
159
  logicalId,
134
160
  physicalId,
135
- resourceType
161
+ resourceType,
136
162
  });
137
163
  }
138
164
  });
@@ -144,19 +170,30 @@ class IntegrationBuilder extends InfrastructureBuilder {
144
170
  /**
145
171
  * Build integration resources based on ownership decisions
146
172
  */
147
- async buildFromDecisions(decisions, appDefinition, result, usePrismaLayer = true) {
173
+ async buildFromDecisions(
174
+ decisions,
175
+ appDefinition,
176
+ result,
177
+ usePrismaLayer = true
178
+ ) {
148
179
  // Create package config first — needed by all Lambda functions including DLQ processor
149
- const functionPackageConfig = this.createFunctionPackageConfig(usePrismaLayer);
180
+ const functionPackageConfig =
181
+ this.createFunctionPackageConfig(usePrismaLayer);
150
182
 
151
183
  // Create InternalErrorQueue if ownership = STACK
152
- const shouldCreateInternalErrorQueue = decisions.internalErrorQueue.ownership === ResourceOwnership.STACK;
184
+ const shouldCreateInternalErrorQueue =
185
+ decisions.internalErrorQueue.ownership === ResourceOwnership.STACK;
153
186
 
154
187
  if (shouldCreateInternalErrorQueue) {
155
188
  console.log(' → Creating InternalErrorQueue in stack');
156
189
  this.createInternalErrorQueue(result, functionPackageConfig);
157
190
  } else {
158
191
  console.log(' → Using external InternalErrorQueue');
159
- this.useExternalInternalErrorQueue(decisions.internalErrorQueue, result, functionPackageConfig);
192
+ this.useExternalInternalErrorQueue(
193
+ decisions.internalErrorQueue,
194
+ result,
195
+ functionPackageConfig
196
+ );
160
197
  }
161
198
 
162
199
  for (const integration of appDefinition.integrations) {
@@ -166,17 +203,29 @@ class IntegrationBuilder extends InfrastructureBuilder {
166
203
  console.log(`\n Adding integration: ${integrationName}`);
167
204
 
168
205
  // Create Lambda function definitions (serverless template code)
169
- await this.createFunctionDefinitions(integration, functionPackageConfig, result, usePrismaLayer);
206
+ await this.createFunctionDefinitions(
207
+ integration,
208
+ functionPackageConfig,
209
+ result,
210
+ usePrismaLayer
211
+ );
170
212
 
171
213
  // Create or reference SQS queue based on ownership decision
172
- const shouldCreateQueue = queueDecision.ownership === ResourceOwnership.STACK;
214
+ const shouldCreateQueue =
215
+ queueDecision.ownership === ResourceOwnership.STACK;
173
216
 
174
217
  if (shouldCreateQueue) {
175
- console.log(` ✓ Creating ${integrationName}Queue in stack`);
218
+ console.log(
219
+ ` ✓ Creating ${integrationName}Queue in stack`
220
+ );
176
221
  this.createIntegrationQueue(integrationName, result);
177
222
  } else {
178
223
  console.log(` ✓ Using external ${integrationName}Queue`);
179
- this.useExternalIntegrationQueue(integrationName, queueDecision, result);
224
+ this.useExternalIntegrationQueue(
225
+ integrationName,
226
+ queueDecision,
227
+ result
228
+ );
180
229
  }
181
230
  }
182
231
  }
@@ -192,12 +241,14 @@ class IntegrationBuilder extends InfrastructureBuilder {
192
241
  'node_modules/@aws-sdk/**',
193
242
 
194
243
  // Exclude Prisma (provided via Lambda Layer)
195
- ...(usePrismaLayer ? [
196
- 'node_modules/@prisma/**',
197
- 'node_modules/.prisma/**',
198
- 'node_modules/prisma/**',
199
- 'node_modules/@friggframework/core/generated/**',
200
- ] : []),
244
+ ...(usePrismaLayer
245
+ ? [
246
+ 'node_modules/@prisma/**',
247
+ 'node_modules/.prisma/**',
248
+ 'node_modules/prisma/**',
249
+ 'node_modules/@friggframework/core/generated/**',
250
+ ]
251
+ : []),
201
252
 
202
253
  // Exclude ALL nested node_modules
203
254
  'node_modules/**/node_modules/**',
@@ -253,21 +304,31 @@ class IntegrationBuilder extends InfrastructureBuilder {
253
304
  * Create Lambda function definitions for an integration
254
305
  * These are serverless framework template function definitions
255
306
  */
256
- async createFunctionDefinitions(integration, functionPackageConfig, result, usePrismaLayer = true) {
307
+ async createFunctionDefinitions(
308
+ integration,
309
+ functionPackageConfig,
310
+ result,
311
+ usePrismaLayer = true
312
+ ) {
257
313
  const integrationName = integration.Definition.name;
258
314
 
259
315
  // Add webhook handler if enabled (BEFORE catch-all proxy route)
260
316
  // CRITICAL: Webhook routes must be defined before the catch-all {proxy+} route
261
317
  // to ensure proper route matching in AWS API Gateway/HTTP API
262
318
  const webhookConfig = integration.Definition.webhooks;
263
- if (webhookConfig && (webhookConfig === true || webhookConfig.enabled === true)) {
319
+ if (
320
+ webhookConfig &&
321
+ (webhookConfig === true || webhookConfig.enabled === true)
322
+ ) {
264
323
  const webhookFunctionName = `${integrationName}Webhook`;
265
324
 
266
325
  result.functions[webhookFunctionName] = {
267
326
  handler: `node_modules/@friggframework/core/handlers/routers/integration-webhook-routers.handlers.${integrationName}Webhook.handler`,
268
- skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
327
+ skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
269
328
  package: functionPackageConfig,
270
- ...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }), // Webhook handlers need Prisma for credential lookups
329
+ ...(usePrismaLayer && {
330
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
331
+ }), // Webhook handlers need Prisma for credential lookups
271
332
  events: [
272
333
  {
273
334
  httpApi: {
@@ -289,9 +350,9 @@ class IntegrationBuilder extends InfrastructureBuilder {
289
350
  // Create HTTP API handler for integration (catch-all route AFTER webhooks)
290
351
  result.functions[integrationName] = {
291
352
  handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
292
- skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
353
+ skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
293
354
  package: functionPackageConfig,
294
- ...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }), // HTTP handlers need Prisma for integration queries
355
+ ...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }), // HTTP handlers need Prisma for integration queries
295
356
  events: [
296
357
  {
297
358
  httpApi: {
@@ -307,20 +368,25 @@ class IntegrationBuilder extends InfrastructureBuilder {
307
368
  const queueWorkerName = `${integrationName}QueueWorker`;
308
369
  result.functions[queueWorkerName] = {
309
370
  handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
310
- skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
371
+ skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
311
372
  package: functionPackageConfig,
312
- ...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }), // Queue workers need Prisma for database operations
313
- reservedConcurrency: 5,
373
+ ...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }), // Queue workers need Prisma for database operations
374
+ reservedConcurrency: 20,
314
375
  events: [
315
376
  {
316
377
  sqs: {
317
- arn: { 'Fn::GetAtt': [`${this.capitalizeFirst(integrationName)}Queue`, 'Arn'] },
378
+ arn: {
379
+ 'Fn::GetAtt': [
380
+ `${this.capitalizeFirst(integrationName)}Queue`,
381
+ 'Arn',
382
+ ],
383
+ },
318
384
  batchSize: 1,
319
385
  functionResponseType: 'ReportBatchItemFailures',
320
386
  },
321
387
  },
322
388
  ],
323
- timeout: 900, // 15 minutes max for queue workers (Lambda maximum)
389
+ timeout: 900, // 15 minutes max for queue workers (Lambda maximum)
324
390
  };
325
391
  console.log(` ✓ Queue worker function defined`);
326
392
  }
@@ -329,20 +395,30 @@ class IntegrationBuilder extends InfrastructureBuilder {
329
395
  * Create InternalErrorQueue CloudFormation resource
330
396
  */
331
397
  createInternalErrorQueue(result, functionPackageConfig) {
398
+ const queueName =
399
+ '${self:service}-${self:provider.stage}-InternalErrorQueue';
400
+
401
+ result.custom.InternalErrorQueue = queueName;
402
+
332
403
  result.resources.InternalErrorQueue = {
333
404
  Type: 'AWS::SQS::Queue',
334
405
  Properties: {
335
- QueueName: '${self:service}-${self:provider.stage}-InternalErrorQueue',
406
+ QueueName: '${self:custom.InternalErrorQueue}',
336
407
  MessageRetentionPeriod: 1209600, // 14 days
337
408
  VisibilityTimeout: 300, // 5 minutes — must be >= 6x DLQ processor Lambda timeout (30s × 6 = 180s)
338
409
  },
339
410
  };
340
411
 
341
- this.createDLQObservability(result, functionPackageConfig, {
342
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
343
- }, {
344
- 'Fn::GetAtt': ['InternalErrorQueue', 'QueueName'],
345
- });
412
+ this.createDLQObservability(
413
+ result,
414
+ functionPackageConfig,
415
+ {
416
+ 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
417
+ },
418
+ {
419
+ 'Fn::GetAtt': ['InternalErrorQueue', 'QueueName'],
420
+ }
421
+ );
346
422
 
347
423
  console.log(' ✓ Created InternalErrorQueue resource');
348
424
  }
@@ -358,9 +434,16 @@ class IntegrationBuilder extends InfrastructureBuilder {
358
434
  const arnParts = decision.physicalId.split(':');
359
435
  const queueName = arnParts[arnParts.length - 1];
360
436
 
361
- this.createDLQObservability(result, functionPackageConfig, decision.physicalId, queueName);
437
+ this.createDLQObservability(
438
+ result,
439
+ functionPackageConfig,
440
+ decision.physicalId,
441
+ queueName
442
+ );
362
443
 
363
- console.log(` ✓ Using external InternalErrorQueue: ${decision.physicalId}`);
444
+ console.log(
445
+ ` ✓ Using external InternalErrorQueue: ${decision.physicalId}`
446
+ );
364
447
  }
365
448
 
366
449
  /**
@@ -372,7 +455,8 @@ class IntegrationBuilder extends InfrastructureBuilder {
372
455
  result.resources.DLQMessageAlarm = {
373
456
  Type: 'AWS::CloudWatch::Alarm',
374
457
  Properties: {
375
- AlarmDescription: 'Messages in dead-letter queue — integration queue processing failures',
458
+ AlarmDescription:
459
+ 'Messages in dead-letter queue — integration queue processing failures',
376
460
  Namespace: 'AWS/SQS',
377
461
  MetricName: 'ApproximateNumberOfMessagesVisible',
378
462
  Statistic: 'Maximum',
@@ -381,15 +465,14 @@ class IntegrationBuilder extends InfrastructureBuilder {
381
465
  EvaluationPeriods: 1,
382
466
  Period: 300,
383
467
  AlarmActions: [{ Ref: 'InternalErrorBridgeTopic' }],
384
- Dimensions: [
385
- { Name: 'QueueName', Value: queueName },
386
- ],
468
+ Dimensions: [{ Name: 'QueueName', Value: queueName }],
387
469
  },
388
470
  };
389
471
 
390
472
  // DLQ processor Lambda: logs failed messages with structured context
391
473
  result.functions.dlqProcessor = {
392
- handler: 'node_modules/@friggframework/core/handlers/workers/dlq-processor.dlqProcessor',
474
+ handler:
475
+ 'node_modules/@friggframework/core/handlers/workers/dlq-processor.dlqProcessor',
393
476
  skipEsbuild: true,
394
477
  package: functionPackageConfig,
395
478
  reservedConcurrency: 1,
@@ -447,7 +530,8 @@ class IntegrationBuilder extends InfrastructureBuilder {
447
530
  */
448
531
  useExternalIntegrationQueue(integrationName, decision, result) {
449
532
  // Add queue URL to environment for Lambda functions
450
- result.environment[`${integrationName.toUpperCase()}_QUEUE_URL`] = decision.physicalId;
533
+ result.environment[`${integrationName.toUpperCase()}_QUEUE_URL`] =
534
+ decision.physicalId;
451
535
 
452
536
  console.log(` ✓ Using external queue: ${decision.physicalId}`);
453
537
  }
@@ -260,7 +260,7 @@ describe('IntegrationBuilder', () => {
260
260
 
261
261
  const result = await integrationBuilder.build(appDefinition, {});
262
262
 
263
- expect(result.functions.testQueueWorker.reservedConcurrency).toBe(5);
263
+ expect(result.functions.testQueueWorker.reservedConcurrency).toBe(20);
264
264
  });
265
265
 
266
266
  it('should add queue URL to environment variables', async () => {
@@ -1162,7 +1162,7 @@ describe('composeServerlessDefinition', () => {
1162
1162
  // Check Queue Worker
1163
1163
  expect(result.functions.testIntegrationQueueWorker).toEqual({
1164
1164
  handler: 'node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.testIntegration.queueWorker',
1165
- reservedConcurrency: 5,
1165
+ reservedConcurrency: 20,
1166
1166
  events: [{
1167
1167
  sqs: {
1168
1168
  arn: {
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-next.76",
4
+ "version": "2.0.0-next.78",
5
5
  "bin": {
6
6
  "frigg": "./frigg-cli/index.js"
7
7
  },
@@ -25,9 +25,9 @@
25
25
  "@babel/eslint-parser": "^7.18.9",
26
26
  "@babel/parser": "^7.25.3",
27
27
  "@babel/traverse": "^7.25.3",
28
- "@friggframework/core": "2.0.0-next.76",
29
- "@friggframework/schemas": "2.0.0-next.76",
30
- "@friggframework/test": "2.0.0-next.76",
28
+ "@friggframework/core": "2.0.0-next.78",
29
+ "@friggframework/schemas": "2.0.0-next.78",
30
+ "@friggframework/test": "2.0.0-next.78",
31
31
  "@hapi/boom": "^10.0.1",
32
32
  "@inquirer/prompts": "^5.3.8",
33
33
  "axios": "^1.7.2",
@@ -55,8 +55,8 @@
55
55
  "validate-npm-package-name": "^5.0.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@friggframework/eslint-config": "2.0.0-next.76",
59
- "@friggframework/prettier-config": "2.0.0-next.76",
58
+ "@friggframework/eslint-config": "2.0.0-next.78",
59
+ "@friggframework/prettier-config": "2.0.0-next.78",
60
60
  "aws-sdk-client-mock": "^4.1.0",
61
61
  "aws-sdk-client-mock-jest": "^4.1.0",
62
62
  "jest": "^30.1.3",
@@ -88,5 +88,5 @@
88
88
  "publishConfig": {
89
89
  "access": "public"
90
90
  },
91
- "gitHead": "5f03113234df57301443502f2751f1864dd44e73"
91
+ "gitHead": "c8e0c0c725a9e49b76d4e095927a443640bf9ec9"
92
92
  }