@friggframework/devtools 2.0.0--canary.590.c1af198.0 → 2.0.0--canary.596.97a5c6b.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.
|
@@ -347,46 +347,15 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
347
347
|
console.log(` ✓ Webhook handler function defined`);
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
-
// Tier 3 Integration Extension routes (e.g. hubspot.extensions.webhooks).
|
|
351
|
-
// The integration-defined-routers handler already mounts these at
|
|
352
|
-
// runtime; emitting explicit httpApi events here registers them as
|
|
353
|
-
// dedicated routes (visible at startup, and matched ahead of the
|
|
354
|
-
// catch-all) instead of silently relying on {proxy+}. They point at
|
|
355
|
-
// the same handler as the catch-all. Route shape mirrors core's
|
|
356
|
-
// getExtensionRoutes; we read it directly to avoid coupling the
|
|
357
|
-
// build-time generator to a core runtime import.
|
|
358
|
-
const extensionBindings = Object.values(
|
|
359
|
-
integration.Definition.extensions || {}
|
|
360
|
-
);
|
|
361
|
-
const extensionRouteEvents = [];
|
|
362
|
-
for (const binding of extensionBindings) {
|
|
363
|
-
const routes =
|
|
364
|
-
(binding && binding.extension && binding.extension.routes) ||
|
|
365
|
-
[];
|
|
366
|
-
for (const route of routes) {
|
|
367
|
-
extensionRouteEvents.push({
|
|
368
|
-
httpApi: {
|
|
369
|
-
path: `/api/${integrationName}-integration${route.path}`,
|
|
370
|
-
method: route.method,
|
|
371
|
-
},
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
if (extensionRouteEvents.length > 0) {
|
|
376
|
-
console.log(
|
|
377
|
-
` ✓ ${extensionRouteEvents.length} extension route(s) defined`
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
350
|
// Create HTTP API handler for integration (catch-all route AFTER
|
|
382
|
-
// webhooks
|
|
351
|
+
// webhooks). Tier 3 extension routes are NOT folded in here — each
|
|
352
|
+
// binding gets its own function below so it can have its own DB infra.
|
|
383
353
|
result.functions[integrationName] = {
|
|
384
354
|
handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
|
|
385
355
|
skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
|
|
386
356
|
package: functionPackageConfig,
|
|
387
357
|
...(usePrismaLayer && { layers: [{ Ref: 'PrismaLambdaLayer' }] }), // HTTP handlers need Prisma for integration queries
|
|
388
358
|
events: [
|
|
389
|
-
...extensionRouteEvents,
|
|
390
359
|
{
|
|
391
360
|
httpApi: {
|
|
392
361
|
path: `/api/${integrationName}-integration/{proxy+}`,
|
|
@@ -397,6 +366,62 @@ class IntegrationBuilder extends InfrastructureBuilder {
|
|
|
397
366
|
};
|
|
398
367
|
console.log(` ✓ HTTP handler function defined`);
|
|
399
368
|
|
|
369
|
+
// Tier 3 Integration Extensions — one dedicated function per binding,
|
|
370
|
+
// namespaced under /{bindingKey} so multiple modules' extensions (e.g.
|
|
371
|
+
// hubspot + clockwork webhooks) never collide on the same path. The
|
|
372
|
+
// Prisma layer is attached only when the extension/binding declares
|
|
373
|
+
// useDatabase: true (default false → DB-free receiver, faster cold
|
|
374
|
+
// start). Route shape mirrors core's getExtensionRoutes; read directly
|
|
375
|
+
// to avoid coupling the build-time generator to a core runtime import.
|
|
376
|
+
const sanitizeBindingKey = (name) =>
|
|
377
|
+
String(name).replace(/[^A-Za-z0-9]/g, '');
|
|
378
|
+
const extensionEntries = Object.entries(
|
|
379
|
+
integration.Definition.extensions || {}
|
|
380
|
+
);
|
|
381
|
+
for (const [bindingKey, binding] of extensionEntries) {
|
|
382
|
+
const extension = binding && binding.extension;
|
|
383
|
+
const routes = (extension && extension.routes) || [];
|
|
384
|
+
if (routes.length === 0) continue;
|
|
385
|
+
const useDatabase =
|
|
386
|
+
binding.useDatabase ??
|
|
387
|
+
(extension && extension.useDatabase) ??
|
|
388
|
+
false;
|
|
389
|
+
// fnName is the wire contract with core's integration-defined-routers
|
|
390
|
+
// (it exports handlers[`${name}__${sanitizeBindingKey(binding)}`]).
|
|
391
|
+
// Keep this derivation IN SYNC with that file.
|
|
392
|
+
const fnName = `${integrationName}__${sanitizeBindingKey(
|
|
393
|
+
bindingKey
|
|
394
|
+
)}`;
|
|
395
|
+
// Guard the same silent-overwrite the runtime router guards: two
|
|
396
|
+
// binding keys that sanitize to the same value would clobber one
|
|
397
|
+
// function definition while leaving both httpApi paths live.
|
|
398
|
+
if (
|
|
399
|
+
Object.prototype.hasOwnProperty.call(result.functions, fnName)
|
|
400
|
+
) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Integration "${integrationName}" extension function conflict: ` +
|
|
403
|
+
`binding "${bindingKey}" sanitizes to "${fnName}", which is already taken. ` +
|
|
404
|
+
`Use binding keys that are distinct after stripping non-alphanumeric characters.`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
result.functions[fnName] = {
|
|
408
|
+
handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${fnName}.handler`,
|
|
409
|
+
skipEsbuild: true,
|
|
410
|
+
package: functionPackageConfig,
|
|
411
|
+
...(usePrismaLayer &&
|
|
412
|
+
useDatabase && { layers: [{ Ref: 'PrismaLambdaLayer' }] }),
|
|
413
|
+
events: routes.map((route) => ({
|
|
414
|
+
httpApi: {
|
|
415
|
+
path: `/api/${integrationName}-integration/${bindingKey}${route.path}`,
|
|
416
|
+
method: route.method,
|
|
417
|
+
},
|
|
418
|
+
})),
|
|
419
|
+
};
|
|
420
|
+
console.log(
|
|
421
|
+
` ✓ Extension handler function defined: ${fnName} (useDatabase: ${useDatabase})`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
400
425
|
// Create Queue Worker function
|
|
401
426
|
const queueWorkerName = `${integrationName}QueueWorker`;
|
|
402
427
|
result.functions[queueWorkerName] = {
|
|
@@ -568,7 +568,7 @@ describe('IntegrationBuilder', () => {
|
|
|
568
568
|
]);
|
|
569
569
|
});
|
|
570
570
|
|
|
571
|
-
it('
|
|
571
|
+
it('emits a dedicated per-binding function (namespaced) for Tier 3 extension routes; the main handler keeps only {proxy+}', async () => {
|
|
572
572
|
const appDefinition = {
|
|
573
573
|
integrations: [
|
|
574
574
|
{
|
|
@@ -602,15 +602,24 @@ describe('IntegrationBuilder', () => {
|
|
|
602
602
|
|
|
603
603
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
604
604
|
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
|
|
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([
|
|
608
613
|
{
|
|
609
614
|
httpApi: {
|
|
610
|
-
path: '/api/hubspot-integration/webhooks',
|
|
615
|
+
path: '/api/hubspot-integration/hubspotWebhooks/webhooks',
|
|
611
616
|
method: 'POST',
|
|
612
617
|
},
|
|
613
618
|
},
|
|
619
|
+
]);
|
|
620
|
+
|
|
621
|
+
// The main integration handler keeps only the catch-all.
|
|
622
|
+
expect(result.functions.hubspot.events).toEqual([
|
|
614
623
|
{
|
|
615
624
|
httpApi: {
|
|
616
625
|
path: '/api/hubspot-integration/{proxy+}',
|
|
@@ -618,6 +627,69 @@ describe('IntegrationBuilder', () => {
|
|
|
618
627
|
},
|
|
619
628
|
},
|
|
620
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/);
|
|
621
693
|
});
|
|
622
694
|
|
|
623
695
|
it('should only have the catch-all proxy route when no extensions are declared', async () => {
|
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.596.97a5c6b.0",
|
|
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--canary.
|
|
29
|
-
"@friggframework/schemas": "2.0.0--canary.
|
|
30
|
-
"@friggframework/test": "2.0.0--canary.
|
|
28
|
+
"@friggframework/core": "2.0.0--canary.596.97a5c6b.0",
|
|
29
|
+
"@friggframework/schemas": "2.0.0--canary.596.97a5c6b.0",
|
|
30
|
+
"@friggframework/test": "2.0.0--canary.596.97a5c6b.0",
|
|
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--canary.
|
|
59
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
58
|
+
"@friggframework/eslint-config": "2.0.0--canary.596.97a5c6b.0",
|
|
59
|
+
"@friggframework/prettier-config": "2.0.0--canary.596.97a5c6b.0",
|
|
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": "
|
|
91
|
+
"gitHead": "97a5c6bc4217a0fcaaa6e6fa7baeb4b179f06d41"
|
|
92
92
|
}
|