feat(server): run server-exposed logic functions in the owner workspace (#22002)
## Summary Implements the server-level logic-function tier in the simplest shape: a logic function is "server-exposed" iff its manifest entry carries `serverWebhookTriggerSettings`. Execution delegates to the owner-workspace copy of that function — billing, throttling, env vars, and the existing executor all apply uniformly against that workspace. Supersedes #21971 with the simplified design from that discussion (no `applicationRegistrationLogicFunction` registry, no dedicated manifest type, no separate SDK helper, no special throttling). ## Design - **Manifest**: `LogicFunctionManifest` gains `serverWebhookTriggerSettings?`. The declarative `workspaceIdResolver` shape is dropped. - **Materialization**: those settings become two new jsonb columns on `LogicFunctionEntity`. The manifest → flat converter and the create-from-source DTO/util forward them; the property-config map and editable-properties list are extended. - **Lookup**: a single QB query joins `logicFunction → application → applicationRegistration` and filters on `lf.workspaceId = reg.workspaceId` to get only the owner workspace's copy. - **Webhook**: `POST /webhooks/server/:logicFunctionUniversalIdentifier` → `ServerWebhookTriggerService.handle` → join lookup → `LogicFunctionTriggerService.run`. No registry table, no `:applicationRegistrationUniversalIdentifier` segment, no resolver. - **Gate**: `IS_SERVER_LOGIC_FUNCTION_ENABLED` config var (disabled by default). ## Test plan - [x] `npx jest server-webhook-trigger` — 9 unit tests across the webhook service. - [x] `npx jest logic-function` — 88 existing tests stay green. - [x] `npx nx typecheck twenty-server`. - [x] `npx nx lint:diff-with-main twenty-server`. - [x] Reset DB → init → run `database:migrate:prod` → run `database:migrate:generate --name pending-migration-check` → no drift. - [ ] Manual: hit `/webhooks/server/<uid>` end-to-end against a manifest carrying `serverWebhookTriggerSettings`. https://claude.ai/code/session_01GgsnCGmYJ26xRirx8va1Yh --- _Generated by [Claude Code](https://claude.ai/code/session_01GgsnCGmYJ26xRirx8va1Yh)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22002?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+42
@@ -198,4 +198,46 @@ describe('defineLogicFunction', () => {
|
||||
'Database event trigger must have an eventName',
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts a serverRouteTriggerSettings resolver returning { workspaceId, targetLogicFunctionUniversalIdentifier }', () => {
|
||||
const result = defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'Resolve workspace from request',
|
||||
serverRouteTriggerSettings: { forwardedRequestHeaders: ['x-tenant'] },
|
||||
handler: async () => ({
|
||||
workspaceId: 'ws-1',
|
||||
targetLogicFunctionUniversalIdentifier: 'target-uid',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.config.serverRouteTriggerSettings).toBeDefined();
|
||||
});
|
||||
|
||||
it('compile-time rejects a serverRouteTriggerSettings resolver returning the wrong shape', () => {
|
||||
// @ts-expect-error — handler must return { workspaceId: string;
|
||||
// targetLogicFunctionUniversalIdentifier: string } when
|
||||
// `serverRouteTriggerSettings` is set.
|
||||
const result = defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'Bad resolver',
|
||||
serverRouteTriggerSettings: { forwardedRequestHeaders: [] },
|
||||
handler: async () => ({ notAWorkspaceId: 'oops' }),
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('compile-time rejects a serverRouteTriggerSettings resolver returning only workspaceId', () => {
|
||||
// @ts-expect-error — handler must also return
|
||||
// `targetLogicFunctionUniversalIdentifier`.
|
||||
const result = defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'Resolver missing target',
|
||||
serverRouteTriggerSettings: { forwardedRequestHeaders: [] },
|
||||
handler: async () => ({ workspaceId: 'ws-1' }),
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,43 @@
|
||||
import { type LogicFunctionManifest } from 'twenty-shared/application';
|
||||
import {
|
||||
type LogicFunctionManifest,
|
||||
type ServerRouteTriggerSettings,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
export type LogicFunctionHandler = (...args: any[]) => any | Promise<any>;
|
||||
|
||||
export type LogicFunctionConfig = Omit<
|
||||
// A resolver function attached to `serverRouteTriggerSettings` runs in the
|
||||
// owner workspace and must return BOTH the target workspace and the target
|
||||
// logic function to dispatch to. The server contract is
|
||||
// `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string;
|
||||
// payload?: object }`. The resolver is the single point of authorization —
|
||||
// the URL only carries the resolver's universalIdentifier.
|
||||
export type ServerRouteResolverResult = {
|
||||
workspaceId: string;
|
||||
targetLogicFunctionUniversalIdentifier: string;
|
||||
payload?: object;
|
||||
};
|
||||
|
||||
export type ServerRouteResolverHandler = (
|
||||
...args: any[]
|
||||
) => ServerRouteResolverResult | Promise<ServerRouteResolverResult>;
|
||||
|
||||
type LogicFunctionConfigBase = Omit<
|
||||
LogicFunctionManifest,
|
||||
| 'sourceHandlerPath'
|
||||
| 'builtHandlerPath'
|
||||
| 'builtHandlerChecksum'
|
||||
| 'handlerName'
|
||||
> & {
|
||||
handler: LogicFunctionHandler;
|
||||
};
|
||||
| 'serverRouteTriggerSettings'
|
||||
>;
|
||||
|
||||
export type LogicFunctionConfig = LogicFunctionConfigBase &
|
||||
(
|
||||
| {
|
||||
serverRouteTriggerSettings?: undefined;
|
||||
handler: LogicFunctionHandler;
|
||||
}
|
||||
| {
|
||||
serverRouteTriggerSettings: ServerRouteTriggerSettings;
|
||||
handler: ServerRouteResolverHandler;
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user