[REQUIRES_FULL_CACHE_FLUSH_WHEN_RELEASED] Refactor FlatEntity to be UniversalFlatEntity superset (#17452)

# Introduction
In this PR we're refactoring the `FlatEntity` type to become a superset
of the `UniversalFlatEntity`.
Right now we're storing all the extra properties in `__universal`
property, at some point it might just be sibling to other entity and we
might rely on the `propertiesToCompare` constants and TypeScript
allowing passing a superset type into a smaller subset type

## FromTo utils
The entity to flat entity method now computes the universal information,
standardized a typing and pattern to do

## Example
Also strictly type
```ts
    "bbb019ea-6205-498c-aea5-67bc53bce8a9": {
      "workspaceId": "20202020-1c25-4d02-bf25-6aeccf7ea419",
      "universalIdentifier": "20202020-d111-4d11-8d11-da5ab0a11002",
      "applicationId": "d01b010d-b984-465b-b40b-370e954e5188",
      "id": "bbb019ea-6205-498c-aea5-67bc53bce8a9",
      "pageLayoutTabId": "791a512f-169f-4209-b731-aa86716668c6",
      "title": "Deals by Company",
      "type": "GRAPH",
      "objectMetadataId": "9e14efea-df5b-4c0e-aba9-cfe455f32397",
      "gridPosition": { "row": 0, "column": 6, "rowSpan": 6, "columnSpan": 6 },
      "configuration": {
        "color": "orange",
        "orderBy": "FIELD_ASC",
        "timezone": "UTC",
        "displayLegend": true,
        "displayDataLabel": false,
        "showCenterMetric": true,
        "configurationType": "PIE_CHART",
        "firstDayOfTheWeek": 0,
        "aggregateOperation": "COUNT",
        "groupBySubFieldName": "name",
        "groupByFieldMetadataId": "6673ff18-63d2-47a1-8f85-2b9b09ca27a5",
        "aggregateFieldMetadataId": "8d64ee41-5dd4-4de6-945a-7c0c18399715"
      },
      "createdAt": "2026-01-28T14:08:52.140Z",
      "updatedAt": "2026-01-28T14:08:52.140Z",
      "deletedAt": null,
      "__universal": {
        "universalIdentifier": "20202020-d111-4d11-8d11-da5ab0a11002",
        "applicationUniversalIdentifier": "20202020-64aa-4b6f-b003-9c74b97cee20",
        "pageLayoutTabUniversalIdentifier": "20202020-d011-4d11-8d11-da5ab0a01001",
        "objectMetadataUniversalIdentifier": "20202020-9549-49dd-b2b2-883999db8938",
        "gridPosition": {
          "row": 0,
          "column": 6,
          "rowSpan": 6,
          "columnSpan": 6
        },
        "configuration": {
          "color": "orange",
          "orderBy": "FIELD_ASC",
          "timezone": "UTC",
          "displayLegend": true,
          "displayDataLabel": false,
          "showCenterMetric": true,
          "configurationType": "PIE_CHART",
          "firstDayOfTheWeek": 0,
          "aggregateOperation": "COUNT",
          "groupBySubFieldName": "name",
          "aggregateFieldMetadataUniversalIdentifier": "20202020-d01a-4131-8a31-f123456789ab",
          "groupByFieldMetadataUniversalIdentifier": "20202020-cbac-457e-b565-adece5fc815f"
        }
      }
    },
```
This commit is contained in:
Paul Rastoin
2026-01-28 16:46:34 +00:00
committed by GitHub
parent 81eaee81a8
commit fe9d6f34ff
90 changed files with 3048 additions and 528 deletions
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
@@ -13,7 +14,7 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/function-build.module';
import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.module';
import { LogicFunctionTriggerJob } from 'src/engine/metadata-modules/logic-function/jobs/logic-function-trigger.job';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
@@ -21,16 +22,16 @@ import { LogicFunctionResolver } from 'src/engine/metadata-modules/logic-functio
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/logic-function.service';
import { LogicFunctionV2Service } from 'src/engine/metadata-modules/logic-function/services/logic-function-v2.service';
import { WorkspaceFlatLogicFunctionMapCacheService } from 'src/engine/metadata-modules/logic-function/services/workspace-flat-logic-function-map-cache.service';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/function-build.module';
@Module({
imports: [
FileUploadModule,
NestjsQueryTypeOrmModule.forFeature([LogicFunctionEntity]),
TypeOrmModule.forFeature([FeatureFlagEntity]),
TypeOrmModule.forFeature([ApplicationEntity, FeatureFlagEntity]),
FileModule,
ThrottlerModule,
ApplicationModule,
@@ -5,12 +5,14 @@ import { Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { fromLogicFunctionEntityToFlatLogicFunction } from 'src/engine/metadata-modules/logic-function/utils/from-logic-function-entity-to-flat-logic-function.util';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@@ -21,6 +23,8 @@ export class WorkspaceFlatLogicFunctionMapCacheService extends WorkspaceCachePro
constructor(
@InjectRepository(LogicFunctionEntity)
private readonly logicFunctionRepository: Repository<LogicFunctionEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {
super();
}
@@ -28,16 +32,27 @@ export class WorkspaceFlatLogicFunctionMapCacheService extends WorkspaceCachePro
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatLogicFunction>> {
const logicFunctions = await this.logicFunctionRepository.find({
where: { workspaceId },
withDeleted: true,
});
const [logicFunctions, applications] = await Promise.all([
this.logicFunctionRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const flatLogicFunctionMaps = createEmptyFlatEntityMaps();
for (const logicFunctionEntity of logicFunctions) {
const flatLogicFunction =
fromLogicFunctionEntityToFlatLogicFunction(logicFunctionEntity);
const flatLogicFunction = fromLogicFunctionEntityToFlatLogicFunction({
entity: logicFunctionEntity,
applicationIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatLogicFunction,
@@ -1,14 +1,32 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import { type LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromLogicFunctionEntityToFlatLogicFunction = ({
entity: logicFunctionEntity,
applicationIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'logicFunction'>): FlatLogicFunction => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
logicFunctionEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${logicFunctionEntity.applicationId} not found for logicFunction ${logicFunctionEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
export const fromLogicFunctionEntityToFlatLogicFunction = (
logicFunctionEntity: LogicFunctionEntity,
): FlatLogicFunction => {
const logicFunctionWithoutRelations = removePropertiesFromRecord(
logicFunctionEntity,
['logicFunctionLayer', 'application'],
getMetadataEntityRelationProperties('logicFunction'),
);
return {
@@ -16,6 +34,15 @@ export const fromLogicFunctionEntityToFlatLogicFunction = (
createdAt: logicFunctionEntity.createdAt.toISOString(),
updatedAt: logicFunctionEntity.updatedAt.toISOString(),
deletedAt: logicFunctionEntity.deletedAt?.toISOString() ?? null,
universalIdentifier: logicFunctionEntity.universalIdentifier,
__universal: {
universalIdentifier: logicFunctionEntity.universalIdentifier,
applicationUniversalIdentifier,
publishedVersions: logicFunctionEntity.publishedVersions,
toolInputSchema: logicFunctionEntity.toolInputSchema,
cronTriggerSettings: logicFunctionEntity.cronTriggerSettings,
databaseEventTriggerSettings:
logicFunctionEntity.databaseEventTriggerSettings,
httpRouteTriggerSettings: logicFunctionEntity.httpRouteTriggerSettings,
},
};
};