feat: emit metadata events for schema changes with actor context for webhooks (#17622)
## Summary
This PR adds **metadata eventing**: when schema metadata
(objectMetadata, fieldMetadata, view, viewField, etc.) is created,
updated, or deleted, we now emit events that can trigger webhooks and
future audit logs. It also adds **actor context** (`userId`,
`workspaceMemberId`) to those events so subscribers can attribute
changes to a user or API key.
## What changed
### 1. Metadata eventing (first commit)
- **MetadataEventEmitter**
New service that emits batch events after successful workspace
migrations. Event names follow `metadata.{entity}.{action}` (e.g.
`metadata.objectMetadata.created`, `metadata.fieldMetadata.updated`).
- **MetadataEventsToDbListener**
Listens for metadata events and enqueues webhook delivery via
`CallWebhookJobsForMetadataJob`.
- **Event types** (twenty-shared)
`MetadataEventAction`, `MetadataEventBatch`, and record event types for
create/update/delete.
- **WorkspaceMigrationValidateBuildAndRunService**
Calls the metadata event emitter after running migrations so all
metadata changes (from any module) emit events from a single place.
- **Create events**
Sourced from the create action payload (`flatEntity` /
`flatFieldMetadatas`) because `fromToAllFlatEntityMaps` does not provide
a before/after diff for creates. Update/delete events still use the
fromToAllFlatEntityMaps comparison.
### 2. Actor context (second commit)
- **MetadataEventEmitter**
Accepts optional `actorContext` (`userId`, `workspaceMemberId`) and
includes it on emitted batch events.
- **WorkspaceMigrationValidateBuildAndRunService**
Passes `actorContext` from the request into the metadata event emitter.
- **Metadata resolvers & services**
All metadata modules resolve `@AuthUser({ allowUndefined: true })` and
`@AuthUserWorkspaceId()` and pass `userId` and `workspaceMemberId`
through to the migration/event pipeline. Both are optional so
API-key–authenticated requests (no user) still emit events without a
user identity.
Shared some questions on Discord about the PR.
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
+3
-25
@@ -1,16 +1,10 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { WebhookEntitySelect } from '@/settings/developers/components/WebhookEntitySelect';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconBox,
|
||||
IconNorthStar,
|
||||
IconPlus,
|
||||
IconTrash,
|
||||
useIcons,
|
||||
} from 'twenty-ui/display';
|
||||
import { IconBox, IconNorthStar, IconPlus, IconTrash } from 'twenty-ui/display';
|
||||
import { IconButton, type SelectOption } from 'twenty-ui/input';
|
||||
|
||||
const OBJECT_DROPDOWN_WIDTH = 240;
|
||||
@@ -51,19 +45,6 @@ export const SettingsDatabaseEventsForm = ({
|
||||
}) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const objectOptions: SelectOption<string>[] = [
|
||||
{ label: t`All Objects`, value: '*', Icon: IconNorthStar },
|
||||
...objectMetadataItems.map((item) => ({
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
Icon: getIcon(item.icon),
|
||||
})),
|
||||
];
|
||||
|
||||
const getActionOptions = (
|
||||
updatedFields?: string[],
|
||||
): SelectOption<string>[] => {
|
||||
@@ -86,15 +67,12 @@ export const SettingsDatabaseEventsForm = ({
|
||||
<>
|
||||
{events.map((operation, index) => (
|
||||
<StyledFilterRow key={index} isMobile={isMobile}>
|
||||
<Select
|
||||
<WebhookEntitySelect
|
||||
dropdownId={`object-webhook-type-select-${index}`}
|
||||
value={operation.object}
|
||||
options={objectOptions}
|
||||
onChange={(newValue) =>
|
||||
updateOperation?.(index, 'object', newValue)
|
||||
}
|
||||
fullWidth
|
||||
emptyOption={{ label: t`Object`, value: null }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Select
|
||||
|
||||
Reference in New Issue
Block a user