Move view in metadata-modules/ and create atomic folder + module for each view entity (#14990)

# Introduction
Preparing view-filter and view-group introduction in v2 core engine
Moving view from `core-modules` to `metadata-modules`

## What happened

### Created dedicated modules for each view entity:
- ViewFieldModule
- ViewFilterModule
- ViewFilterGroupModule
- ViewGroupModule
- ViewSortModule

### Each module is now completely independent with its own:
- Controller
- Resolver
- Service
- Entity

### Created dedicated abstraction metadata module folder for:
- flat-view-field
- flat-view

### Dependencies 
- Eleminated circular dep on ViewModule to all others ones
- Granular import not importing the whole viewModule anymore everywhere


close https://github.com/twentyhq/core-team-issues/issues/1703
This commit is contained in:
Paul Rastoin
2025-10-09 15:18:15 +02:00
committed by GitHub
parent c9a1a110e1
commit 59fbe35a8c
198 changed files with 877 additions and 765 deletions
@@ -0,0 +1,8 @@
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
export const FLAT_VIEW_FIELD_EDITABLE_PROPERTIES = [
'isVisible',
'size',
'position',
'aggregateOperation',
] as const satisfies (keyof FlatViewField)[];
@@ -0,0 +1,7 @@
import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
export const FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE = [
...FLAT_VIEW_FIELD_EDITABLE_PROPERTIES,
'deletedAt',
] as const satisfies (keyof FlatViewField)[];
@@ -0,0 +1,7 @@
import { type ViewFieldEntityRelationProperties } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
export const VIEW_FIELD_ENTITY_RELATION_PROPERTIES = [
'fieldMetadata',
'view',
'workspace',
] as const satisfies ViewFieldEntityRelationProperties[];
@@ -0,0 +1,50 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
import { fromViewFieldEntityToFlatViewField } from 'src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator';
import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service';
@Injectable()
@WorkspaceFlatMapCache('flatViewFieldMaps')
export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceFlatMapCacheService<FlatViewFieldMaps> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(ViewFieldEntity)
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
) {
super(cacheStorageService);
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatViewFieldMaps> {
const existingViewFields = await this.viewFieldRepository.find({
where: {
workspaceId,
},
withDeleted: true,
});
return existingViewFields.reduce((flatViewFieldMaps, viewFieldEntity) => {
const flatViewField = fromViewFieldEntityToFlatViewField(viewFieldEntity);
return addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatViewField,
flatEntityMaps: flatViewFieldMaps,
});
}, EMPTY_FLAT_ENTITY_MAPS);
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
export type FlatViewFieldMaps = FlatEntityMaps<FlatViewField>;
@@ -0,0 +1,4 @@
import { type FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-properties-to-compare.constant';
export type FlatViewFieldPropertiesToCompare =
(typeof FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE)[number];
@@ -0,0 +1,18 @@
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
export type ViewFieldEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
ViewFieldEntity,
FieldMetadataEntity | ViewEntity | Workspace
>;
export type FlatViewField = Omit<
ViewFieldEntity,
ViewFieldEntityRelationProperties
> & {
universalIdentifier: string;
};
@@ -0,0 +1,59 @@
import diff from 'microdiff';
import { type FromTo } from 'twenty-shared/types';
import { FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-properties-to-compare.constant';
import { type FlatViewFieldPropertiesToCompare } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-properties-to-compare.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type UpdateViewFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
type GetWorkspaceMigrationUpdateViewFieldActionArgs = FromTo<
FlatViewField,
'FlatViewField'
>;
export const compareTwoFlatViewField = ({
fromFlatViewField,
toFlatViewField,
}: GetWorkspaceMigrationUpdateViewFieldActionArgs) => {
const transformMetadataForComparisonParameters = {
shouldIgnoreProperty: (property: string) =>
!FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE.includes(
property as FlatViewFieldPropertiesToCompare,
),
propertiesToStringify: [],
};
const fromCompare = transformMetadataForComparison(
fromFlatViewField,
transformMetadataForComparisonParameters,
);
const toCompare = transformMetadataForComparison(
toFlatViewField,
transformMetadataForComparisonParameters,
);
const flatViewFieldDifferences = diff(fromCompare, toCompare);
return flatViewFieldDifferences.flatMap<
UpdateViewFieldAction['updates'][number]
>((difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0] as FlatViewFieldPropertiesToCompare;
return {
from: oldValue,
to: value,
property,
};
}
case 'CREATE':
case 'REMOVE':
default: {
// Should never occurs, we should only provide null never undefined and so on
return [];
}
}
});
};
@@ -0,0 +1,38 @@
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input';
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/constants/DEFAULT_VIEW_FIELD_SIZE';
export const fromCreateViewFieldInputToFlatViewFieldToCreate = ({
createViewFieldInput: rawCreateViewFieldInput,
workspaceId,
}: {
createViewFieldInput: CreateViewFieldInput;
workspaceId: string;
}): FlatViewField => {
const { fieldMetadataId, viewId, ...createViewFieldInput } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawCreateViewFieldInput,
['aggregateOperation', 'fieldMetadataId', 'id', 'viewId'],
);
const createdAt = new Date();
const viewFieldId = createViewFieldInput.id ?? v4();
return {
id: viewFieldId,
fieldMetadataId,
viewId,
workspaceId,
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: null,
universalIdentifier: viewFieldId,
isVisible: createViewFieldInput.isVisible ?? true,
size: createViewFieldInput.size ?? DEFAULT_VIEW_FIELD_SIZE,
position: createViewFieldInput.position ?? 0,
aggregateOperation: createViewFieldInput.aggregateOperation ?? null,
};
};
@@ -0,0 +1,40 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type DeleteViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/delete-view-field.input';
import {
ViewFieldException,
ViewFieldExceptionCode,
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
export const fromDeleteViewFieldInputToFlatViewFieldOrThrow = ({
deleteViewFieldInput: rawDeleteViewFieldInput,
flatViewFieldMaps,
}: {
deleteViewFieldInput: DeleteViewFieldInput;
flatViewFieldMaps: FlatViewFieldMaps;
}): FlatViewField => {
const { id: viewFieldId } = extractAndSanitizeObjectStringFields(
rawDeleteViewFieldInput,
['id'],
);
const existingFlatViewFieldToDelete = flatViewFieldMaps.byId[viewFieldId];
if (!isDefined(existingFlatViewFieldToDelete)) {
throw new ViewFieldException(
t`View field to delete not found`,
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
return {
...existingFlatViewFieldToDelete,
deletedAt: new Date(),
};
};
@@ -0,0 +1,37 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type DestroyViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/destroy-view-field.input';
import {
ViewFieldException,
ViewFieldExceptionCode,
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
export const fromDestroyViewFieldInputToFlatViewFieldOrThrow = ({
destroyViewFieldInput,
flatViewFieldMaps,
}: {
destroyViewFieldInput: DestroyViewFieldInput;
flatViewFieldMaps: FlatViewFieldMaps;
}): FlatViewField => {
const { id: viewFieldId } = extractAndSanitizeObjectStringFields(
destroyViewFieldInput,
['id'],
);
const existingFlatViewFieldToDestroy = flatViewFieldMaps.byId[viewFieldId];
if (!isDefined(existingFlatViewFieldToDestroy)) {
throw new ViewFieldException(
t`View field to destroy not found`,
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
return existingFlatViewFieldToDestroy;
};
@@ -0,0 +1,50 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant';
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
import {
ViewFieldException,
ViewFieldExceptionCode,
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export const fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow = ({
updateViewFieldInput: rawUpdateViewFieldInput,
flatViewFieldMaps,
}: {
updateViewFieldInput: UpdateViewFieldInput;
flatViewFieldMaps: FlatViewFieldMaps;
}): FlatViewField => {
const { id: viewFieldToUpdateId } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawUpdateViewFieldInput,
['id'],
);
const existingFlatViewFieldToUpdate =
flatViewFieldMaps.byId[viewFieldToUpdateId];
if (!isDefined(existingFlatViewFieldToUpdate)) {
throw new ViewFieldException(
t`View field to update not found`,
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
rawUpdateViewFieldInput.update,
FLAT_VIEW_FIELD_EDITABLE_PROPERTIES,
);
return mergeUpdateInExistingRecord({
existing: existingFlatViewFieldToUpdate,
properties: FLAT_VIEW_FIELD_EDITABLE_PROPERTIES,
update: updatedEditableFieldProperties,
});
};
@@ -0,0 +1,21 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { VIEW_FIELD_ENTITY_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/view-field-entity-relation-properties.constant';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
export const fromViewFieldEntityToFlatViewField = (
viewFieldEntity: ViewFieldEntity,
): FlatViewField => {
const viewFieldEntityWithoutRelations = removePropertiesFromRecord(
viewFieldEntity,
VIEW_FIELD_ENTITY_RELATION_PROPERTIES,
);
return {
...viewFieldEntityWithoutRelations,
universalIdentifier:
viewFieldEntityWithoutRelations.universalIdentifier ??
viewFieldEntityWithoutRelations.id,
};
};
@@ -0,0 +1,17 @@
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
export const isViewFieldInLowestPosition = ({
flatViewField,
otherFlatViewFields,
}: {
otherFlatViewFields: FlatViewField[];
flatViewField: FlatViewField;
}) => {
if (otherFlatViewFields.length === 0) {
return true;
}
const positions = otherFlatViewFields.map(({ position }) => position);
const lowestPosition = Math.min(...positions);
return flatViewField.position < lowestPosition;
};