@friggframework/devtools 2.0.0-next.87 → 2.0.0-next.89

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.
@@ -347,7 +347,8 @@ class IntegrationBuilder extends InfrastructureBuilder {
347
347
  console.log(` ✓ Webhook handler function defined`);
348
348
  }
349
349
 
350
- // Create HTTP API handler for integration (catch-all route AFTER webhooks)
350
+ // Create HTTP API handler for integration (catch-all route AFTER
351
+ // webhooks). Extension routes get their own functions below.
351
352
  result.functions[integrationName] = {
352
353
  handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
353
354
  skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
@@ -364,6 +365,54 @@ class IntegrationBuilder extends InfrastructureBuilder {
364
365
  };
365
366
  console.log(` ✓ HTTP handler function defined`);
366
367
 
368
+ // One serverless function per extension binding, namespaced under
369
+ // /{bindingKey}. Prisma layer attached only when useDatabase is true.
370
+ const sanitizeBindingKey = (name) =>
371
+ String(name).replace(/[^A-Za-z0-9]/g, '');
372
+ const extensionEntries = Object.entries(
373
+ integration.Definition.extensions || {}
374
+ );
375
+ for (const [bindingKey, binding] of extensionEntries) {
376
+ const extension = binding && binding.extension;
377
+ const routes = (extension && extension.routes) || [];
378
+ if (routes.length === 0) continue;
379
+ const useDatabase =
380
+ binding.useDatabase ??
381
+ (extension && extension.useDatabase) ??
382
+ false;
383
+ // Wire contract: core's integration-defined-routers derives the
384
+ // identical handler key. Keep both in sync.
385
+ const fnName = `${integrationName}__${sanitizeBindingKey(
386
+ bindingKey
387
+ )}`;
388
+ // Distinct binding keys can sanitize to the same fnName — fail loud rather than overwrite.
389
+ if (
390
+ Object.prototype.hasOwnProperty.call(result.functions, fnName)
391
+ ) {
392
+ throw new Error(
393
+ `Integration "${integrationName}" extension function conflict: ` +
394
+ `binding "${bindingKey}" sanitizes to "${fnName}", which is already taken. ` +
395
+ `Use binding keys that are distinct after stripping non-alphanumeric characters.`
396
+ );
397
+ }
398
+ result.functions[fnName] = {
399
+ handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${fnName}.handler`,
400
+ skipEsbuild: true,
401
+ package: functionPackageConfig,
402
+ ...(usePrismaLayer &&
403
+ useDatabase && { layers: [{ Ref: 'PrismaLambdaLayer' }] }),
404
+ events: routes.map((route) => ({
405
+ httpApi: {
406
+ path: `/api/${integrationName}-integration/${bindingKey}${route.path}`,
407
+ method: route.method,
408
+ },
409
+ })),
410
+ };
411
+ console.log(
412
+ ` ✓ Extension handler function defined: ${fnName} (useDatabase: ${useDatabase})`
413
+ );
414
+ }
415
+
367
416
  // Create Queue Worker function
368
417
  const queueWorkerName = `${integrationName}QueueWorker`;
369
418
  result.functions[queueWorkerName] = {
@@ -568,6 +568,147 @@ describe('IntegrationBuilder', () => {
568
568
  ]);
569
569
  });
570
570
 
571
+ it('emits a dedicated per-binding function (namespaced) for Tier 3 extension routes; the main handler keeps only {proxy+}', async () => {
572
+ const appDefinition = {
573
+ integrations: [
574
+ {
575
+ Definition: {
576
+ name: 'hubspot',
577
+ extensions: {
578
+ hubspotWebhooks: {
579
+ extension: {
580
+ name: 'hubspot-webhooks',
581
+ routes: [
582
+ {
583
+ path: '/webhooks',
584
+ method: 'POST',
585
+ event: 'HUBSPOT_WEBHOOK_RECEIVED',
586
+ },
587
+ ],
588
+ events: {
589
+ HUBSPOT_WEBHOOK_RECEIVED: {
590
+ type: 'LIFE_CYCLE_EVENT',
591
+ handler: () => {},
592
+ },
593
+ },
594
+ },
595
+ handlers: {},
596
+ },
597
+ },
598
+ },
599
+ },
600
+ ],
601
+ };
602
+
603
+ const result = await integrationBuilder.build(appDefinition, {});
604
+
605
+ // Dedicated per-binding function, namespaced under the binding key,
606
+ // pointing at its own handler export.
607
+ const fn = result.functions.hubspot__hubspotWebhooks;
608
+ expect(fn).toBeDefined();
609
+ expect(fn.handler).toBe(
610
+ 'node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.hubspot__hubspotWebhooks.handler'
611
+ );
612
+ expect(fn.events).toEqual([
613
+ {
614
+ httpApi: {
615
+ path: '/api/hubspot-integration/hubspotWebhooks/webhooks',
616
+ method: 'POST',
617
+ },
618
+ },
619
+ ]);
620
+
621
+ // The main integration handler keeps only the catch-all.
622
+ expect(result.functions.hubspot.events).toEqual([
623
+ {
624
+ httpApi: {
625
+ path: '/api/hubspot-integration/{proxy+}',
626
+ method: 'ANY',
627
+ },
628
+ },
629
+ ]);
630
+
631
+ // useDatabase defaults to false → no Prisma layer on the receiver.
632
+ expect(fn.layers).toBeUndefined();
633
+ });
634
+
635
+ it('attaches the Prisma layer to a per-binding function only when useDatabase is true', async () => {
636
+ const mkDef = (useDatabase) => ({
637
+ integrations: [
638
+ {
639
+ Definition: {
640
+ name: 'hs',
641
+ extensions: {
642
+ wh: {
643
+ extension: {
644
+ name: 'wh-ext',
645
+ useDatabase,
646
+ routes: [
647
+ {
648
+ path: '/webhooks',
649
+ method: 'POST',
650
+ event: 'E',
651
+ },
652
+ ],
653
+ events: { E: { handler: () => {} } },
654
+ },
655
+ },
656
+ },
657
+ },
658
+ },
659
+ ],
660
+ });
661
+
662
+ const withDb = await integrationBuilder.build(mkDef(true), {});
663
+ expect(withDb.functions.hs__wh.layers).toEqual([
664
+ { Ref: 'PrismaLambdaLayer' },
665
+ ]);
666
+
667
+ const withoutDb = await integrationBuilder.build(mkDef(false), {});
668
+ expect(withoutDb.functions.hs__wh.layers).toBeUndefined();
669
+ });
670
+
671
+ it('throws when two binding keys sanitize to the same function name', async () => {
672
+ const mkExt = (name, event) => ({
673
+ name,
674
+ routes: [{ path: '/w', method: 'POST', event }],
675
+ events: { [event]: { handler: () => {} } },
676
+ });
677
+ const appDefinition = {
678
+ integrations: [
679
+ {
680
+ Definition: {
681
+ name: 'hs',
682
+ extensions: {
683
+ 'hub-spot': { extension: mkExt('a', 'A') }, // → hs__hubspot
684
+ hubspot: { extension: mkExt('b', 'B') }, // → hs__hubspot
685
+ },
686
+ },
687
+ },
688
+ ],
689
+ };
690
+ await expect(
691
+ integrationBuilder.build(appDefinition, {})
692
+ ).rejects.toThrow(/extension function conflict.*hs__hubspot/);
693
+ });
694
+
695
+ it('should only have the catch-all proxy route when no extensions are declared', async () => {
696
+ const appDefinition = {
697
+ integrations: [{ Definition: { name: 'plain' } }],
698
+ };
699
+
700
+ const result = await integrationBuilder.build(appDefinition, {});
701
+
702
+ expect(result.functions.plain.events).toEqual([
703
+ {
704
+ httpApi: {
705
+ path: '/api/plain-integration/{proxy+}',
706
+ method: 'ANY',
707
+ },
708
+ },
709
+ ]);
710
+ });
711
+
571
712
  it('should define webhook handler BEFORE catch-all proxy route (ordering bug fix)', async () => {
572
713
  const appDefinition = {
573
714
  integrations: [
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.87",
4
+ "version": "2.0.0-next.89",
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.87",
29
- "@friggframework/schemas": "2.0.0-next.87",
30
- "@friggframework/test": "2.0.0-next.87",
28
+ "@friggframework/core": "2.0.0-next.89",
29
+ "@friggframework/schemas": "2.0.0-next.89",
30
+ "@friggframework/test": "2.0.0-next.89",
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.87",
59
- "@friggframework/prettier-config": "2.0.0-next.87",
58
+ "@friggframework/eslint-config": "2.0.0-next.89",
59
+ "@friggframework/prettier-config": "2.0.0-next.89",
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": "ee72b6f5349ee764da595f67cc6f184a42f763f1"
91
+ "gitHead": "19d4f89c10f2c0214fda28dbbd6c3bd83430da6b"
92
92
  }