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:
@@ -1,4 +1,9 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import {
|
||||
Field,
|
||||
HideField,
|
||||
ObjectType,
|
||||
registerEnumType,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import {
|
||||
@@ -12,6 +17,7 @@ import {
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
|
||||
import { type ViewOverrides } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
|
||||
import { ViewFilterGroupDTO } from 'src/engine/metadata-modules/view-filter-group/dtos/view-filter-group.dto';
|
||||
import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto';
|
||||
@@ -117,4 +123,10 @@ export class ViewDTO {
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
createdByUserWorkspaceId?: string | null;
|
||||
|
||||
@Field(() => Boolean, { nullable: false })
|
||||
isActive: boolean;
|
||||
|
||||
@HideField()
|
||||
overrides?: ViewOverrides | null;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from 'typeorm';
|
||||
import {
|
||||
AggregateOperations,
|
||||
type SerializedRelation,
|
||||
ViewCalendarLayout,
|
||||
ViewKey,
|
||||
ViewOpenRecordIn,
|
||||
@@ -30,7 +31,24 @@ import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-g
|
||||
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
|
||||
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { OverridableEntity } from 'src/engine/workspace-manager/types/overridable-entity';
|
||||
|
||||
export type ViewOverrides = {
|
||||
name?: string;
|
||||
type?: ViewType;
|
||||
icon?: string;
|
||||
position?: number;
|
||||
isCompact?: boolean;
|
||||
openRecordIn?: ViewOpenRecordIn;
|
||||
kanbanAggregateOperation?: AggregateOperations | null;
|
||||
kanbanAggregateOperationFieldMetadataId?: SerializedRelation | null;
|
||||
anyFieldFilterValue?: string | null;
|
||||
calendarLayout?: ViewCalendarLayout | null;
|
||||
calendarFieldMetadataId?: SerializedRelation | null;
|
||||
visibility?: ViewVisibility;
|
||||
mainGroupByFieldMetadataId?: SerializedRelation | null;
|
||||
shouldHideEmptyGroups?: boolean;
|
||||
};
|
||||
|
||||
// We could refactor this type to be dynamic to view type
|
||||
@Entity({ name: 'view', schema: 'core' })
|
||||
@@ -49,7 +67,10 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
'CHK_VIEW_CALENDAR_INTEGRITY',
|
||||
`("type" != 'CALENDAR' OR ("calendarLayout" IS NOT NULL AND "calendarFieldMetadataId" IS NOT NULL))`,
|
||||
)
|
||||
export class ViewEntity extends SyncableEntity implements Required<ViewEntity> {
|
||||
export class ViewEntity
|
||||
extends OverridableEntity<ViewOverrides>
|
||||
implements Required<ViewEntity>
|
||||
{
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { fromCreateViewInputToFlatViewToCreate } from 'src/engine/metadata-modul
|
||||
import { fromDeleteViewInputToFlatViewOrThrow } from 'src/engine/metadata-modules/flat-view/utils/from-delete-view-input-to-flat-view-or-throw.util';
|
||||
import { fromDestroyViewInputToFlatViewOrThrow } from 'src/engine/metadata-modules/flat-view/utils/from-destroy-view-input-to-flat-view-or-throw.util';
|
||||
import { fromUpdateViewInputToFlatViewToUpdateOrThrow } from 'src/engine/metadata-modules/flat-view/utils/from-update-view-input-to-flat-view-to-update-or-throw.util';
|
||||
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
|
||||
import { fromFlatViewFieldGroupToViewFieldGroupDto } from 'src/engine/metadata-modules/view-field-group/utils/from-flat-view-field-group-to-view-field-group-dto.util';
|
||||
import { fromFlatViewFieldToViewFieldDto } from 'src/engine/metadata-modules/view-field/utils/from-flat-view-field-to-view-field-dto.util';
|
||||
import { fromFlatViewFilterGroupToViewFilterGroupDto } from 'src/engine/metadata-modules/view-filter-group/utils/from-flat-view-filter-group-to-view-filter-group-dto.util';
|
||||
@@ -166,6 +167,10 @@ export class ViewService {
|
||||
flatViewGroupMaps: existingFlatViewGroupMaps,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
userWorkspaceId,
|
||||
callerApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
@@ -239,6 +244,10 @@ export class ViewService {
|
||||
fromDeleteViewInputToFlatViewOrThrow({
|
||||
deleteViewInput,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
callerApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
@@ -313,14 +322,29 @@ export class ViewService {
|
||||
flatEntityMaps: existingFlatViewMaps,
|
||||
});
|
||||
|
||||
const shouldDeactivate = isCallerOverridingEntity({
|
||||
callerApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
entityApplicationUniversalIdentifier:
|
||||
existingFlatView.applicationUniversalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
});
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
view: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [flatViewFromDestroyInput],
|
||||
flatEntityToUpdate: [],
|
||||
flatEntityToDelete: shouldDeactivate
|
||||
? []
|
||||
: [flatViewFromDestroyInput],
|
||||
flatEntityToUpdate: shouldDeactivate
|
||||
? [{ ...existingFlatView, isActive: false, updatedAt: now }]
|
||||
: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
@@ -337,9 +361,17 @@ export class ViewService {
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldDeactivate) {
|
||||
return fromFlatViewToViewDto({
|
||||
...existingFlatView,
|
||||
isActive: false,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
return fromFlatViewToViewDto({
|
||||
...existingFlatView,
|
||||
deletedAt: new Date().toISOString(),
|
||||
deletedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -2,10 +2,11 @@ import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-
|
||||
import { type ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto';
|
||||
|
||||
export const fromFlatViewToViewDto = (flatView: FlatView): ViewDTO => {
|
||||
const { createdAt, updatedAt, deletedAt, ...rest } = flatView;
|
||||
const { createdAt, updatedAt, deletedAt, overrides, ...rest } = flatView;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...(overrides ?? {}),
|
||||
createdAt: new Date(createdAt),
|
||||
updatedAt: new Date(updatedAt),
|
||||
deletedAt: deletedAt ? new Date(deletedAt) : null,
|
||||
|
||||
Reference in New Issue
Block a user