@friggframework/core 2.0.0--canary.596.97a5c6b.0 → 2.0.0--canary.597.ea460ba.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/core/create-handler.js
CHANGED
|
@@ -49,7 +49,6 @@ const createHandler = (optionByName = {}) => {
|
|
|
49
49
|
eventName = 'Event',
|
|
50
50
|
isUserFacingResponse = true,
|
|
51
51
|
method,
|
|
52
|
-
shouldUseDatabase = true,
|
|
53
52
|
} = optionByName;
|
|
54
53
|
|
|
55
54
|
if (!method) {
|
|
@@ -80,15 +79,6 @@ const createHandler = (optionByName = {}) => {
|
|
|
80
79
|
// If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
|
|
81
80
|
await secretsToEnv();
|
|
82
81
|
|
|
83
|
-
// Open the database connection up front when the handler needs it.
|
|
84
|
-
// Lazy-required so DB-free handlers (e.g. extension webhook
|
|
85
|
-
// receivers with useDatabase:false) never load the Prisma client.
|
|
86
|
-
// $connect is idempotent, so this safely reuses a warm connection.
|
|
87
|
-
if (shouldUseDatabase) {
|
|
88
|
-
const { connectPrisma } = require('../database/prisma');
|
|
89
|
-
await connectPrisma();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
82
|
// Helps reuse the database connection. Lowers response times.
|
|
93
83
|
context.callbackWaitsForEmptyEventLoop = false;
|
|
94
84
|
|
|
@@ -511,10 +511,6 @@ router.get('/health/ready', async (_req, res) => {
|
|
|
511
511
|
});
|
|
512
512
|
});
|
|
513
513
|
|
|
514
|
-
|
|
515
|
-
// /health/live is a pure liveness probe (no DB), and /health/ready probes the
|
|
516
|
-
// DB itself and degrades to 503 gracefully. Eager-connect at handler entry
|
|
517
|
-
// would turn a DB outage into a 500 for both (killing healthy containers).
|
|
518
|
-
const handler = createAppHandler('HTTP Event: Health', router, false);
|
|
514
|
+
const handler = createAppHandler('HTTP Event: Health', router);
|
|
519
515
|
|
|
520
516
|
module.exports = { handler, router };
|
|
@@ -12,9 +12,6 @@ const { integrations: integrationClasses } = loadAppDefinition();
|
|
|
12
12
|
|
|
13
13
|
const routeKey = (method, path) => `${(method || 'ANY').toUpperCase()} ${path}`;
|
|
14
14
|
|
|
15
|
-
// Serverless function keys must be alphanumeric; binding keys are developer-chosen.
|
|
16
|
-
const sanitizeBindingKey = (name) => String(name).replace(/[^A-Za-z0-9]/g, '');
|
|
17
|
-
|
|
18
15
|
//todo: this should be in a use case class
|
|
19
16
|
for (const IntegrationClass of integrationClasses) {
|
|
20
17
|
const router = Router();
|
|
@@ -56,32 +53,20 @@ for (const IntegrationClass of integrationClasses) {
|
|
|
56
53
|
}
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
// Tier 3 Integration Extension routes —
|
|
60
|
-
// handler, namespaced under /{bindingName}, so multiple modules' extensions
|
|
61
|
-
// can declare the same relative path (e.g. two /webhooks) without colliding.
|
|
62
|
-
// Each per-binding handler carries its own shouldUseDatabase (resolved from
|
|
63
|
-
// the extension/binding `useDatabase`, default false → DB-free receiver).
|
|
64
|
-
const bindingGroups = new Map();
|
|
56
|
+
// Tier 3 Integration Extension routes — see EXTENSIONS.md
|
|
65
57
|
for (const extRoute of getExtensionRoutes(IntegrationClass)) {
|
|
66
|
-
const namespacedPath = `/${extRoute.bindingName}${extRoute.path}`;
|
|
67
58
|
claim(
|
|
68
59
|
extRoute.method,
|
|
69
|
-
|
|
60
|
+
extRoute.path,
|
|
70
61
|
`extension "${extRoute.extensionName}" (binding "${extRoute.bindingName}")`
|
|
71
62
|
);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
router: Router(),
|
|
75
|
-
useDatabase: extRoute.useDatabase,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
const group = bindingGroups.get(extRoute.bindingName);
|
|
79
|
-
group.router.use(
|
|
80
|
-
`${basePath}/${extRoute.bindingName}`,
|
|
63
|
+
router.use(
|
|
64
|
+
basePath,
|
|
81
65
|
loadRouterFromObject(IntegrationClass, extRoute)
|
|
82
66
|
);
|
|
67
|
+
const method = extRoute.method.toUpperCase();
|
|
83
68
|
console.log(
|
|
84
|
-
`│ ${
|
|
69
|
+
`│ ${method} ${basePath}${extRoute.path} (extension: ${extRoute.extensionName})`
|
|
85
70
|
);
|
|
86
71
|
}
|
|
87
72
|
console.log('│');
|
|
@@ -92,33 +77,6 @@ for (const IntegrationClass of integrationClasses) {
|
|
|
92
77
|
router
|
|
93
78
|
),
|
|
94
79
|
};
|
|
95
|
-
|
|
96
|
-
for (const [bindingName, group] of bindingGroups) {
|
|
97
|
-
// The function key is the wire contract with the devtools serverless
|
|
98
|
-
// generator (integration-builder.js builds the identical key). Keep
|
|
99
|
-
// the derivation here and there IN SYNC.
|
|
100
|
-
const fnKey = `${IntegrationClass.Definition.name}__${sanitizeBindingKey(
|
|
101
|
-
bindingName
|
|
102
|
-
)}`;
|
|
103
|
-
// Two binding keys that sanitize to the same value (e.g. "hub-spot"
|
|
104
|
-
// and "hubspot") would silently overwrite each other's handler. The
|
|
105
|
-
// namespaced-path claim() above can't catch it (the paths differ), so
|
|
106
|
-
// fail loud here.
|
|
107
|
-
if (Object.prototype.hasOwnProperty.call(handlers, fnKey)) {
|
|
108
|
-
throw new Error(
|
|
109
|
-
`Integration "${IntegrationClass.Definition.name}" extension handler conflict: ` +
|
|
110
|
-
`binding "${bindingName}" sanitizes to "${fnKey}", which is already taken. ` +
|
|
111
|
-
`Use binding keys that are distinct after stripping non-alphanumeric characters.`
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
handlers[fnKey] = {
|
|
115
|
-
handler: createAppHandler(
|
|
116
|
-
`HTTP Event: ${IntegrationClass.Definition.name} extension ${bindingName}`,
|
|
117
|
-
group.router,
|
|
118
|
-
group.useDatabase
|
|
119
|
-
),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
80
|
}
|
|
123
81
|
|
|
124
82
|
module.exports = { handlers };
|
|
@@ -24,8 +24,6 @@ class HubSpotIntegration extends IntegrationBase {
|
|
|
24
24
|
hubspotWebhooks: {
|
|
25
25
|
extension: hubspot.extensions.webhooks,
|
|
26
26
|
handlers: { HUBSPOT_WEBHOOK: 'onHubSpotEvent' },
|
|
27
|
-
// optional: override the extension's declared useDatabase
|
|
28
|
-
// useDatabase: true,
|
|
29
27
|
},
|
|
30
28
|
},
|
|
31
29
|
};
|
|
@@ -41,42 +39,19 @@ class HubSpotIntegration extends IntegrationBase {
|
|
|
41
39
|
}
|
|
42
40
|
```
|
|
43
41
|
|
|
44
|
-
The binding key (`hubspotWebhooks`) is your local name.
|
|
42
|
+
The binding key (`hubspotWebhooks`) is your local name. The extension reference (`hubspot.extensions.webhooks`) is whatever the API module exports.
|
|
45
43
|
|
|
46
44
|
## Step 2: Deploy
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
The framework auto-mounts each extension's routes at the integration's base path. Boot logs show:
|
|
49
47
|
|
|
50
48
|
```
|
|
51
49
|
│ Configuring routes for hubspot Integration:
|
|
52
|
-
│ POST /api/hubspot-integration/
|
|
50
|
+
│ POST /api/hubspot-integration/webhooks (extension: hubspot-webhooks)
|
|
53
51
|
│
|
|
54
52
|
```
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
> **⚠️ Breaking:** extension routes used to mount un-namespaced (`/api/{x}-integration/webhooks`). They are now namespaced under the binding key. Any provider webhook already registered against the old path must be re-pointed at the new `/{bindingKey}` URL — and for signature schemes that sign the full URL (e.g. HubSpot v3), the old registration will also fail verification until updated.
|
|
59
|
-
|
|
60
|
-
## `useDatabase` — does the receiver open a DB connection?
|
|
61
|
-
|
|
62
|
-
Each extension declares whether its route handler should open a database connection:
|
|
63
|
-
|
|
64
|
-
```javascript
|
|
65
|
-
// in the extension bundle (api-module side)
|
|
66
|
-
module.exports = {
|
|
67
|
-
name: 'hubspot-webhooks',
|
|
68
|
-
useDatabase: false, // default — the receiver is DB-free
|
|
69
|
-
routes: [ /* ... */ ],
|
|
70
|
-
events: { /* ... */ },
|
|
71
|
-
};
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
- **Default is `false`** — a webhook receiver that only verifies a signature and enqueues should not pay for a DB connection (faster cold start; at build time its Lambda doesn't get the Prisma layer).
|
|
75
|
-
- Set `useDatabase: true` at the **extension level** if the receiver itself needs the DB. A binding may override it locally (`extensions: { x: { extension, useDatabase: true } }`), though that's rarely needed.
|
|
76
|
-
- Resolution order: `binding.useDatabase ?? extension.useDatabase ?? false`.
|
|
77
|
-
- Scope note: `false` is the default **for extension routes**. `createHandler` itself still defaults `shouldUseDatabase: true` for the integration's own catch-all handler and the legacy `Definition.webhooks: true` path — those connect as before. The `false` default applies only to the per-binding extension handler.
|
|
78
|
-
|
|
79
|
-
If `useDatabase` is `false`, the receiver must not touch the database. Work that needs the DB (e.g. resolving `portalId → integrationId`) belongs in the queue worker that processes the dispatched event, not in the receiver.
|
|
54
|
+
Hit that URL and the bound method (`onHubSpotEvent`) fires on the resolved per-account integration instance.
|
|
80
55
|
|
|
81
56
|
## How handler binding works
|
|
82
57
|
|
|
@@ -109,7 +84,7 @@ This means **binding the same extension twice only works if the extension itself
|
|
|
109
84
|
|
|
110
85
|
Subclass overrides via `this.events[eventName]` (set in the constructor) take precedence over extension-declared events. If a binding tried to wire a handler that's now shadowed, the framework logs a warning naming the integration, binding, and ignored method.
|
|
111
86
|
|
|
112
|
-
|
|
87
|
+
Route path conflicts (two extensions declaring the same `method + path`, or an extension colliding with a `Definition.routes` entry) also throw at boot.
|
|
113
88
|
|
|
114
89
|
## Authoring an extension (for API module authors)
|
|
115
90
|
|
|
@@ -70,24 +70,6 @@ function validateExtensionBinding(extension, bindingName, integrationName, bindi
|
|
|
70
70
|
throw new Error(`${ctx}: extension is missing required "name" field`);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
if (
|
|
74
|
-
extension.useDatabase !== undefined &&
|
|
75
|
-
typeof extension.useDatabase !== 'boolean'
|
|
76
|
-
) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
`${ctx}: extension "${extension.name}" "useDatabase" must be a boolean`
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
if (
|
|
82
|
-
binding &&
|
|
83
|
-
binding.useDatabase !== undefined &&
|
|
84
|
-
typeof binding.useDatabase !== 'boolean'
|
|
85
|
-
) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
`${ctx}: binding "useDatabase" must be a boolean`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
73
|
const events = extension.events || {};
|
|
92
74
|
if (typeof events !== 'object' || Array.isArray(events)) {
|
|
93
75
|
throw new Error(
|
|
@@ -199,10 +181,6 @@ function getExtensionRoutes(IntegrationClass) {
|
|
|
199
181
|
integrationName,
|
|
200
182
|
binding
|
|
201
183
|
);
|
|
202
|
-
// useDatabase is resolved at the binding level, falling back to the
|
|
203
|
-
// extension's declared value, then to false (DB-free by default).
|
|
204
|
-
const useDatabase =
|
|
205
|
-
binding.useDatabase ?? binding.extension.useDatabase ?? false;
|
|
206
184
|
const routes = binding.extension.routes || [];
|
|
207
185
|
for (const route of routes) {
|
|
208
186
|
flat.push({
|
|
@@ -211,7 +189,6 @@ function getExtensionRoutes(IntegrationClass) {
|
|
|
211
189
|
path: route.path,
|
|
212
190
|
method: route.method,
|
|
213
191
|
event: route.event,
|
|
214
|
-
useDatabase,
|
|
215
192
|
});
|
|
216
193
|
}
|
|
217
194
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.597.ea460ba.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
42
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
43
|
-
"@friggframework/test": "2.0.0--canary.
|
|
41
|
+
"@friggframework/eslint-config": "2.0.0--canary.597.ea460ba.0",
|
|
42
|
+
"@friggframework/prettier-config": "2.0.0--canary.597.ea460ba.0",
|
|
43
|
+
"@friggframework/test": "2.0.0--canary.597.ea460ba.0",
|
|
44
44
|
"@prisma/client": "^6.17.0",
|
|
45
45
|
"@types/lodash": "4.17.15",
|
|
46
46
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
"publishConfig": {
|
|
81
81
|
"access": "public"
|
|
82
82
|
},
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "ea460ba4cff4f584ecd38c0973dc966f09ee1ca8"
|
|
84
84
|
}
|