[Call Recording] Add standard object (#21158)

Adds **Call Recording** as a first-class standard object (Twenty's
flat-metadata
standard-object system), with a hidden junction to calendar events and a
backfill
command for existing workspaces. Everything is gated behind the
`IS_CALL_RECORDING_ENABLED` feature flag.

### What's included
- **`CallRecording`**: audio/video files, transcript, status, recording
policy,
timing, external bot/recording ids. Label identifier is
`meetingOccurrenceKey`.
- **`CallRecordingCalendarEventAssociation`**: hidden junction linking a
recording
to a calendar event (dedupes one bot to many subscribers of the same
meeting).
- Full metadata graph via the flat-metadata builders: fields, indexes,
views,
  view fields/groups, record page layout, and navigation items.
- **Metadata-only reverse relation** on `CalendarEvent`: present in
standard
metadata, omitted from the TS entity class to avoid expanding recursive
  nested-insert types.
- **Upgrade command (2.9.0)** backfilling active/suspended workspaces:
  - Creates the full graph; idempotent (skips when it already exists).
- Moves a colliding custom `callRecording` object aside to
`callRecordingOld`
    (numeric suffix if that name is also taken).
- Navigation items (commands) are flag-gated by `universalIdentifier`,
so a custom object
    reusing the name is never gated.

### QA
Run locally against existing workspaces (with and without a name
collision) and a
freshly created workspace:
- [x] Backfill, collision: custom `callRecording` renamed to
`callRecordingOld`;
  standard graph created.
- [x] Backfill, no collision: standard graph created; unrelated custom
object untouched.
- [x] Idempotent: re-run is a no-op, with no duplicate metadata and
counts unchanged.
- [x] New workspace via `init()` produces an identical graph to the
backfill
  (`universalIdentifier` set-diff = 0).
- [x] Label identifier (`meetingOccurrenceKey`) holds position 0 in
non-widget views.
- [x] Nav items gated behind the feature flag; collision-renamed
object's nav
  expression re-pointed to its new name.
- [x] Unit tests cover collision name resolution and nav-gating logic.
This commit is contained in:
nitin
2026-06-05 18:32:50 +05:30
committed by GitHub
parent a78b319877
commit e485b679ea
41 changed files with 3999 additions and 899 deletions
@@ -1,8 +1,10 @@
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { v5 } from 'uuid';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
import {
buildNavigationConditionalAvailabilityExpression,
buildNavigationFlatCommandMenuItem,
NAVIGATION_INTERPOLATED_ICON,
NAVIGATION_INTERPOLATED_LABEL,
@@ -114,6 +116,21 @@ describe('buildNavigationFlatCommandMenuItem', () => {
);
});
it('should additionally gate conditionalAvailabilityExpression behind the feature flag for feature-flagged objects', () => {
const result = buildNavigationFlatCommandMenuItem({
...baseArgs,
objectMetadata: {
...baseObjectMetadata,
universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier,
nameSingular: 'callRecording',
},
});
expect(result.conditionalAvailabilityExpression).toBe(
'featureFlags.IS_CALL_RECORDING_ENABLED and targetObjectReadPermissions.callRecording',
);
});
it('should set isPinned to false', () => {
const result = buildNavigationFlatCommandMenuItem(baseArgs);
@@ -137,3 +154,34 @@ describe('buildNavigationFlatCommandMenuItem', () => {
expect(result.updatedAt).toBe('2026-01-01T00:00:00.000Z');
});
});
describe('buildNavigationConditionalAvailabilityExpression', () => {
it('gates the standard call recording object behind both the flag and read permission', () => {
expect(
buildNavigationConditionalAvailabilityExpression({
universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier,
nameSingular: 'callRecording',
}),
).toBe(
'featureFlags.IS_CALL_RECORDING_ENABLED and targetObjectReadPermissions.callRecording',
);
});
it('returns only the read-permission expression for non-gated objects', () => {
expect(
buildNavigationConditionalAvailabilityExpression({
universalIdentifier: 'obj-universal-1',
nameSingular: 'person',
}),
).toBe('targetObjectReadPermissions.person');
});
it('does not gate a custom object that reuses the callRecording name', () => {
expect(
buildNavigationConditionalAvailabilityExpression({
universalIdentifier: 'custom-object-universal-id',
nameSingular: 'callRecording',
}),
).toBe('targetObjectReadPermissions.callRecording');
});
});
@@ -1,3 +1,5 @@
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v5 } from 'uuid';
@@ -16,6 +18,31 @@ export const NAVIGATION_INTERPOLATED_SHORT_LABEL =
export const NAVIGATION_INTERPOLATED_ICON =
'${navigateToObjectMetadataItem.icon}';
const NAVIGATION_FEATURE_FLAG_GATE_BY_OBJECT_UNIVERSAL_IDENTIFIER: Partial<
Record<string, FeatureFlagKey>
> = {
[STANDARD_OBJECTS.callRecording.universalIdentifier]:
FeatureFlagKey.IS_CALL_RECORDING_ENABLED,
};
export const buildNavigationConditionalAvailabilityExpression = ({
universalIdentifier,
nameSingular,
}: {
universalIdentifier: string;
nameSingular: string;
}): string => {
const targetObjectReadPermissionExpression = `targetObjectReadPermissions.${nameSingular}`;
const featureFlagGate =
NAVIGATION_FEATURE_FLAG_GATE_BY_OBJECT_UNIVERSAL_IDENTIFIER[
universalIdentifier
];
return isDefined(featureFlagGate)
? `featureFlags.${featureFlagGate} and ${targetObjectReadPermissionExpression}`
: targetObjectReadPermissionExpression;
};
export const buildNavigationFlatCommandMenuItem = ({
objectMetadata,
commandMenuItemId,
@@ -41,6 +68,12 @@ export const buildNavigationFlatCommandMenuItem = ({
NAVIGATION_COMMAND_UUID_NAMESPACE,
);
const conditionalAvailabilityExpression =
buildNavigationConditionalAvailabilityExpression({
universalIdentifier: objectMetadata.universalIdentifier,
nameSingular: objectMetadata.nameSingular,
});
return {
id: commandMenuItemId,
universalIdentifier,
@@ -54,7 +87,7 @@ export const buildNavigationFlatCommandMenuItem = ({
position,
isPinned: false,
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: `targetObjectReadPermissions.${objectMetadata.nameSingular}`,
conditionalAvailabilityExpression,
frontComponentId: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,