feat(server): convert view to overridable entity (#21436)
## Context Every entity created as a side effect of object creation must support the overridable pattern (`isActive` + `overrides` + override routing) before we can re-own side effects to their true application. Starting with View. viewField, viewFieldGroup, pageLayoutTab and pageLayoutWidget already extend `OverridableEntity`. This PR brings `view` to the same pattern. ## What this does - `ViewEntity` now extends `OverridableEntity<ViewOverrides>` (adds `isActive` boolean + `overrides` jsonb). All editable view properties are overridable; the 3 fieldMetadata foreign keys are converted to/from universal identifiers like viewField's `viewFieldGroupId`. - **Update**: mutations on a view not owned by the caller (e.g. standard views like "All Companies") are written into `overrides` instead of mutating the row. Reads merge overrides in the DTO. - **Delete/destroy**: views not owned by the caller are deactivated (`isActive = false`) instead of deleted. ~~- **INDEX invariant**: `key = INDEX` views can only be created via object-creation side effect. The API now rejects creating, deleting or destroying INDEX views (object-deletion cascade is unaffected). This was not really needed for this migration but was flagged during implementation.~~ - **Front**: views with `isActive = false` are filtered out of the views selector. - Fast instance command adds the two columns (`2-12-instance-command-fast-...-view-overridable-entity.ts`). ## Notes - Custom (caller-owned) views behave exactly as before: direct updates, soft delete. - View-group side effects (kanban groups) are computed on the override-merged view so overridden `mainGroupByFieldMetadataId` works.
This commit is contained in:
+3
-1
@@ -12,7 +12,9 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
|
||||
settings: 'settings',
|
||||
},
|
||||
objectMetadata: {},
|
||||
view: {},
|
||||
view: {
|
||||
overrides: 'overrides',
|
||||
},
|
||||
viewField: {
|
||||
overrides: 'overrides',
|
||||
},
|
||||
|
||||
+2
@@ -64,6 +64,8 @@ type Assertions = [
|
||||
| 'anyFieldFilterValue'
|
||||
| 'visibility'
|
||||
| 'createdByUserWorkspaceId'
|
||||
| 'isActive'
|
||||
| 'universalOverrides'
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
+10
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
FlatCreateViewAction,
|
||||
UniversalCreateViewAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/types/workspace-migration-view-action.type';
|
||||
import { fromUniversalOverridesToViewOverrides } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/utils/from-universal-overrides-to-view-overrides.util';
|
||||
import {
|
||||
WorkspaceMigrationActionRunnerArgs,
|
||||
WorkspaceMigrationActionRunnerContext,
|
||||
@@ -41,6 +43,13 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi
|
||||
universalForeignKeyValues: action.flatEntity,
|
||||
});
|
||||
|
||||
const overrides = isDefined(action.flatEntity.universalOverrides)
|
||||
? fromUniversalOverridesToViewOverrides({
|
||||
universalOverrides: action.flatEntity.universalOverrides,
|
||||
flatFieldMetadataMaps: allFlatEntityMaps.flatFieldMetadataMaps,
|
||||
})
|
||||
: null;
|
||||
|
||||
const emptyUniversalForeignKeyAggregators =
|
||||
getUniversalFlatEntityEmptyForeignKeyAggregators({
|
||||
metadataName: 'view',
|
||||
@@ -54,6 +63,7 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi
|
||||
kanbanAggregateOperationFieldMetadataId,
|
||||
mainGroupByFieldMetadataId,
|
||||
objectMetadataId,
|
||||
overrides,
|
||||
id: action.id ?? v4(),
|
||||
applicationId: flatApplication.id,
|
||||
workspaceId,
|
||||
|
||||
+20
-5
@@ -9,6 +9,7 @@ import {
|
||||
FlatUpdateViewAction,
|
||||
UniversalUpdateViewAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/types/workspace-migration-view-action.type';
|
||||
import { fromUniversalOverridesToViewOverrides } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/utils/from-universal-overrides-to-view-overrides.util';
|
||||
import {
|
||||
WorkspaceMigrationActionRunnerArgs,
|
||||
WorkspaceMigrationActionRunnerContext,
|
||||
@@ -33,11 +34,25 @@ export class UpdateViewActionHandlerService extends WorkspaceMigrationRunnerActi
|
||||
universalIdentifier: action.universalIdentifier,
|
||||
});
|
||||
|
||||
const update = resolveUniversalUpdateRelationIdentifiersToIds({
|
||||
metadataName: 'view',
|
||||
universalUpdate: action.update,
|
||||
allFlatEntityMaps,
|
||||
});
|
||||
const { universalOverrides, ...updateWithResolvedForeignKeys } =
|
||||
resolveUniversalUpdateRelationIdentifiersToIds({
|
||||
metadataName: 'view',
|
||||
universalUpdate: action.update,
|
||||
allFlatEntityMaps,
|
||||
});
|
||||
|
||||
const update =
|
||||
universalOverrides === undefined
|
||||
? updateWithResolvedForeignKeys
|
||||
: universalOverrides === null
|
||||
? { ...updateWithResolvedForeignKeys, overrides: null }
|
||||
: {
|
||||
...updateWithResolvedForeignKeys,
|
||||
overrides: fromUniversalOverridesToViewOverrides({
|
||||
universalOverrides,
|
||||
flatFieldMetadataMaps: allFlatEntityMaps.flatFieldMetadataMaps,
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'update',
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { type FormatRecordSerializedRelationProperties } from 'twenty-shared/types';
|
||||
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type ViewOverrides } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
|
||||
type UniversalViewOverrides =
|
||||
FormatRecordSerializedRelationProperties<ViewOverrides>;
|
||||
|
||||
const VIEW_OVERRIDES_UNIVERSAL_FIELD_METADATA_PROPERTIES = [
|
||||
'kanbanAggregateOperationFieldMetadataUniversalIdentifier',
|
||||
'calendarFieldMetadataUniversalIdentifier',
|
||||
'mainGroupByFieldMetadataUniversalIdentifier',
|
||||
] as const;
|
||||
|
||||
type ViewOverridesUniversalFieldMetadataProperty =
|
||||
(typeof VIEW_OVERRIDES_UNIVERSAL_FIELD_METADATA_PROPERTIES)[number];
|
||||
|
||||
const toForeignKeyProperty = (
|
||||
universalProperty: ViewOverridesUniversalFieldMetadataProperty,
|
||||
) =>
|
||||
universalProperty.replace(
|
||||
/UniversalIdentifier$/,
|
||||
'Id',
|
||||
) as keyof ViewOverrides;
|
||||
|
||||
export const fromUniversalOverridesToViewOverrides = ({
|
||||
universalOverrides,
|
||||
flatFieldMetadataMaps,
|
||||
}: {
|
||||
universalOverrides: UniversalViewOverrides;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
}): ViewOverrides => {
|
||||
const {
|
||||
kanbanAggregateOperationFieldMetadataUniversalIdentifier: _kanban,
|
||||
calendarFieldMetadataUniversalIdentifier: _calendar,
|
||||
mainGroupByFieldMetadataUniversalIdentifier: _mainGroupBy,
|
||||
...scalarOverrides
|
||||
} = universalOverrides;
|
||||
|
||||
return VIEW_OVERRIDES_UNIVERSAL_FIELD_METADATA_PROPERTIES.reduce<ViewOverrides>(
|
||||
(acc, universalProperty) => {
|
||||
const universalIdentifier = universalOverrides[universalProperty];
|
||||
|
||||
if (universalIdentifier === undefined) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const foreignKeyProperty = toForeignKeyProperty(universalProperty);
|
||||
|
||||
if (universalIdentifier === null) {
|
||||
return { ...acc, [foreignKeyProperty]: null };
|
||||
}
|
||||
|
||||
const flatFieldMetadata =
|
||||
findFlatEntityByUniversalIdentifier<FlatFieldMetadata>({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier,
|
||||
});
|
||||
|
||||
return { ...acc, [foreignKeyProperty]: flatFieldMetadata?.id ?? null };
|
||||
},
|
||||
scalarOverrides,
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user