[BREAKING_CHANGE_VIEW_SORT] Refactor view sort to v2 (#17609)

Fixes https://github.com/twentyhq/core-team-issues/issues/2187

---------

Co-authored-by: prastoin <paul@twenty.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
BOHEUS
2026-03-04 12:48:58 +00:00
committed by GitHub
parent 906a0aed38
commit c97d872b9f
135 changed files with 3262 additions and 815 deletions
@@ -0,0 +1,5 @@
import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
export const FLAT_VIEW_SORT_EDITABLE_PROPERTIES = [
'direction',
] as const satisfies MetadataEntityPropertyName<'viewSort'>[];
@@ -0,0 +1,84 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { FlatViewSortMaps } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type';
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { fromViewSortEntityToFlatViewSort } from 'src/engine/metadata-modules/flat-view-sort/utils/from-view-sort-entity-to-flat-view-sort.util';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
@Injectable()
@WorkspaceCache('flatViewSortMaps')
export class WorkspaceFlatViewSortMapCacheService extends WorkspaceCacheProvider<FlatViewSortMaps> {
constructor(
@InjectRepository(ViewSortEntity)
private readonly viewSortRepository: Repository<ViewSortEntity>,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatViewSortMaps> {
const [existingViewSorts, applications, views, fieldMetadatas] =
await Promise.all([
this.viewSortRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.viewRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const viewIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(views);
const fieldMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(fieldMetadatas);
const flatViewSortMaps = createEmptyFlatEntityMaps();
for (const viewSort of existingViewSorts) {
const flatViewSort = fromViewSortEntityToFlatViewSort({
entity: viewSort,
applicationIdToUniversalIdentifierMap,
viewIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatViewSort,
flatEntityMapsToMutate: flatViewSortMaps,
});
}
return flatViewSortMaps;
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatViewSort } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type';
export type FlatViewSortMaps = FlatEntityMaps<FlatViewSort>;
@@ -0,0 +1,4 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
export type FlatViewSort = FlatEntityFrom<ViewSortEntity>;
@@ -0,0 +1,58 @@
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type CreateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input';
import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type';
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
export const fromCreateViewSortInputToFlatViewSortToCreate = ({
createViewSortInput: rawCreateViewSortInput,
flatApplication,
flatFieldMetadataMaps,
flatViewMaps,
}: {
createViewSortInput: CreateViewSortInput;
flatApplication: FlatApplication;
} & Pick<
AllFlatEntityMaps,
'flatViewMaps' | 'flatFieldMetadataMaps'
>): UniversalFlatViewSort & {
id: string;
} => {
const { viewId, fieldMetadataId, ...createViewSortInput } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawCreateViewSortInput,
['fieldMetadataId', 'id', 'viewId'],
);
const createdAt = new Date().toISOString();
const viewSortId = createViewSortInput.id ?? v4();
const { fieldMetadataUniversalIdentifier, viewUniversalIdentifier } =
resolveEntityRelationUniversalIdentifiers({
metadataName: 'viewSort',
foreignKeyValues: {
fieldMetadataId,
viewId,
},
flatEntityMaps: {
flatFieldMetadataMaps,
flatViewMaps,
},
});
return {
id: viewSortId,
fieldMetadataUniversalIdentifier,
viewUniversalIdentifier,
createdAt,
updatedAt: createdAt,
deletedAt: null,
universalIdentifier: createViewSortInput.universalIdentifier ?? v4(),
direction: createViewSortInput.direction ?? ViewSortDirection.ASC,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
};
};
@@ -0,0 +1,44 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type FlatViewSortMaps } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type';
import { type DeleteViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/delete-view-sort.input';
import {
ViewSortException,
ViewSortExceptionCode,
} from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception';
import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
export const fromDeleteViewSortInputToFlatViewSortOrThrow = ({
deleteViewSortInput: rawDeleteViewSortInput,
flatViewSortMaps,
}: {
deleteViewSortInput: DeleteViewSortInput;
flatViewSortMaps: FlatViewSortMaps;
}): UniversalFlatViewSort => {
const { id: viewSortId } = extractAndSanitizeObjectStringFields(
rawDeleteViewSortInput,
['id'],
);
const existingFlatViewSortToDelete = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: viewSortId,
flatEntityMaps: flatViewSortMaps,
});
if (!isDefined(existingFlatViewSortToDelete)) {
throw new ViewSortException(
t`View sort not found`,
ViewSortExceptionCode.VIEW_SORT_NOT_FOUND,
);
}
return {
...existingFlatViewSortToDelete,
deletedAt: new Date().toISOString(),
};
};
@@ -0,0 +1,41 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type FlatViewSortMaps } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type';
import { type DestroyViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/destroy-view-sort.input';
import {
ViewSortException,
ViewSortExceptionCode,
} from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type';
export const fromDestroyViewSortInputToFlatViewSortOrThrow = ({
destroyViewSortInput,
flatViewSortMaps,
}: {
destroyViewSortInput: DestroyViewSortInput;
flatViewSortMaps: FlatViewSortMaps;
}): UniversalFlatViewSort => {
const { id: viewSortId } = extractAndSanitizeObjectStringFields(
destroyViewSortInput,
['id'],
);
const existingFlatViewSortToDestroy = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: viewSortId,
flatEntityMaps: flatViewSortMaps,
});
if (!isDefined(existingFlatViewSortToDestroy)) {
throw new ViewSortException(
t`View sort not found`,
ViewSortExceptionCode.VIEW_SORT_NOT_FOUND,
);
}
return existingFlatViewSortToDestroy;
};
@@ -0,0 +1,54 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import { FLAT_VIEW_SORT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-sort/constants/flat-view-sort-editable-properties.constant';
import { type UpdateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input';
import {
ViewSortException,
ViewSortExceptionCode,
} from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type';
import { type FlatViewSortMaps } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
export const fromUpdateViewSortInputToFlatViewSortToUpdateOrThrow = ({
updateViewSortInput: rawUpdateViewSortInput,
flatViewSortMaps,
}: {
updateViewSortInput: UpdateViewSortInput;
flatViewSortMaps: FlatViewSortMaps;
}): UniversalFlatViewSort => {
const { id: viewSortToUpdateId } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawUpdateViewSortInput,
['id'],
);
const existingFlatViewSortToUpdate = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: viewSortToUpdateId,
flatEntityMaps: flatViewSortMaps,
});
if (!isDefined(existingFlatViewSortToUpdate)) {
throw new ViewSortException(
t`View sort not found`,
ViewSortExceptionCode.VIEW_SORT_NOT_FOUND,
);
}
const updatedEditableSortProperties = extractAndSanitizeObjectStringFields(
rawUpdateViewSortInput.update,
FLAT_VIEW_SORT_EDITABLE_PROPERTIES,
);
return mergeUpdateInExistingRecord({
existing: existingFlatViewSortToUpdate,
properties: FLAT_VIEW_SORT_EDITABLE_PROPERTIES,
update: updatedEditableSortProperties,
});
};
@@ -0,0 +1,63 @@
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import type { FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FlatViewSort } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type';
export const fromViewSortEntityToFlatViewSort = ({
entity: viewSortEntity,
applicationIdToUniversalIdentifierMap,
viewIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'viewSort'>): FlatViewSort => {
const viewSortEntityWithoutRelations = removePropertiesFromRecord(
viewSortEntity,
getMetadataEntityRelationProperties('viewSort'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(viewSortEntity.applicationId);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${viewSortEntity.applicationId} not found for viewSort ${viewSortEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const viewUniversalIdentifier = viewIdToUniversalIdentifierMap.get(
viewSortEntity.viewId,
);
if (!isDefined(viewUniversalIdentifier)) {
throw new FlatEntityMapsException(
`View with id ${viewSortEntity.viewId} not found for viewSort ${viewSortEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const fieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(viewSortEntity.fieldMetadataId);
if (!isDefined(fieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Field metadata with id ${viewSortEntity.fieldMetadataId} not found for viewSort ${viewSortEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...viewSortEntityWithoutRelations,
createdAt: viewSortEntity.createdAt.toISOString(),
updatedAt: viewSortEntity.updatedAt.toISOString(),
deletedAt: viewSortEntity.deletedAt?.toISOString() ?? null,
universalIdentifier: viewSortEntityWithoutRelations.universalIdentifier,
applicationUniversalIdentifier,
viewUniversalIdentifier,
fieldMetadataUniversalIdentifier,
};
};