[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 17:46:34 +01:00
committed by GitHub
parent 81eaee81a8
commit fe9d6f34ff
90 changed files with 3058 additions and 538 deletions
@@ -0,0 +1,25 @@
import {
type AllFieldMetadataSettings,
type FieldMetadataSettingsMapping,
FieldMetadataType,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const isFieldMetadataSettingsOfType = <
T extends keyof FieldMetadataSettingsMapping,
>(
settings: AllFieldMetadataSettings | null,
fieldMetadataType: T,
): settings is FieldMetadataSettingsMapping[T] => {
// Settings don't have a discriminator - the type is determined by fieldMetadataType
// For required settings types (RELATION, MORPH_RELATION, FILES), ensure settings is defined
if (
fieldMetadataType === FieldMetadataType.RELATION ||
fieldMetadataType === FieldMetadataType.MORPH_RELATION ||
fieldMetadataType === FieldMetadataType.FILES
) {
return isDefined(settings);
}
return true;
};
@@ -1,15 +1,22 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { WorkspaceFlatAgentMapCacheService } from 'src/engine/metadata-modules/flat-agent/services/workspace-flat-agent-map-cache.service';
import { WorkspaceFlatRoleTargetByAgentIdService } from 'src/engine/metadata-modules/flat-agent/services/workspace-flat-role-target-by-agent-id.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
@Module({
imports: [
TypeOrmModule.forFeature([AgentEntity, RoleTargetEntity]),
TypeOrmModule.forFeature([
AgentEntity,
ApplicationEntity,
RoleEntity,
RoleTargetEntity,
]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [
@@ -5,11 +5,13 @@ 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 { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type FlatAgentMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-agent-maps.type';
import { transformAgentEntityToFlatAgent } from 'src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
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()
@@ -18,20 +20,35 @@ export class WorkspaceFlatAgentMapCacheService extends WorkspaceCacheProvider<Fl
constructor(
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatAgentMaps> {
const agents = await this.agentRepository.find({
where: { workspaceId },
withDeleted: true,
});
const [agents, applications] = await Promise.all([
this.agentRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const flatAgentMaps = createEmptyFlatEntityMaps();
for (const agentEntity of agents) {
const flatAgent = transformAgentEntityToFlatAgent(agentEntity);
const flatAgent = transformAgentEntityToFlatAgent({
entity: agentEntity,
applicationIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatAgent,
@@ -6,10 +6,13 @@ import { IsNull, Not, 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 { FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type';
import { fromRoleTargetsEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util';
import { fromRoleTargetEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
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';
@Injectable()
@WorkspaceCache('flatRoleTargetByAgentIdMaps')
@@ -17,6 +20,10 @@ export class WorkspaceFlatRoleTargetByAgentIdService extends WorkspaceCacheProvi
constructor(
@InjectRepository(RoleTargetEntity)
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
) {
super();
}
@@ -24,13 +31,30 @@ export class WorkspaceFlatRoleTargetByAgentIdService extends WorkspaceCacheProvi
async computeForCache(
workspaceId: string,
): Promise<FlatRoleTargetByAgentIdMaps> {
const roleTargetEntities = await this.roleTargetRepository.find({
where: {
workspaceId,
agentId: Not(IsNull()),
},
withDeleted: true,
});
const [roleTargetEntities, applications, roles] = await Promise.all([
this.roleTargetRepository.find({
where: {
workspaceId,
agentId: Not(IsNull()),
},
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.roleRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);
const flatRoleTargetByAgentIdMaps: FlatRoleTargetByAgentIdMaps = {};
@@ -38,8 +62,11 @@ export class WorkspaceFlatRoleTargetByAgentIdService extends WorkspaceCacheProvi
Omit<RoleTargetEntity, 'agentId'> &
NonNullableRequired<Pick<RoleTargetEntity, 'agentId'>>
>) {
const flatRoleTarget =
fromRoleTargetsEntityToFlatRoleTarget(roleTargetEntity);
const flatRoleTarget = fromRoleTargetEntityToFlatRoleTarget({
entity: roleTargetEntity,
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
});
flatRoleTargetByAgentIdMaps[roleTargetEntity.agentId] = flatRoleTarget;
}
@@ -1,9 +1,26 @@
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { isDefined } from 'twenty-shared/utils';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const transformAgentEntityToFlatAgent = ({
entity: agentEntity,
applicationIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'agent'>): FlatAgent => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(agentEntity.applicationId);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${agentEntity.applicationId} not found for agent ${agentEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
export const transformAgentEntityToFlatAgent = (
agentEntity: AgentEntity,
): FlatAgent => {
return {
createdAt: agentEntity.createdAt.toISOString(),
deletedAt: agentEntity.deletedAt?.toISOString() ?? null,
@@ -23,5 +40,11 @@ export const transformAgentEntityToFlatAgent = (
applicationId: agentEntity.applicationId,
modelConfiguration: agentEntity.modelConfiguration,
evaluationInputs: agentEntity.evaluationInputs,
__universal: {
universalIdentifier: agentEntity.universalIdentifier,
applicationUniversalIdentifier,
responseFormat: agentEntity.responseFormat,
modelConfiguration: agentEntity.modelConfiguration,
},
};
};
@@ -1,13 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { WorkspaceFlatCommandMenuItemMapCacheService } from 'src/engine/metadata-modules/flat-command-menu-item/services/workspace-flat-command-menu-item-map-cache.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@Module({
imports: [
TypeOrmModule.forFeature([CommandMenuItemEntity]),
TypeOrmModule.forFeature([
CommandMenuItemEntity,
ApplicationEntity,
ObjectMetadataEntity,
]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatCommandMenuItemMapCacheService],
@@ -5,11 +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 { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { type FlatCommandMenuItemMaps } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item-maps.type';
import { fromCommandMenuItemEntityToFlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-command-menu-item-entity-to-flat-command-menu-item.util';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
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()
@@ -18,21 +21,48 @@ export class WorkspaceFlatCommandMenuItemMapCacheService extends WorkspaceCacheP
constructor(
@InjectRepository(CommandMenuItemEntity)
private readonly commandMenuItemRepository: Repository<CommandMenuItemEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatCommandMenuItemMaps> {
const commandMenuItems = await this.commandMenuItemRepository.find({
where: { workspaceId },
withDeleted: true,
});
const [commandMenuItems, applications, objectMetadatas] = await Promise.all(
[
this.commandMenuItemRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.objectMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
],
);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);
const flatCommandMenuItemMaps = createEmptyFlatEntityMaps();
for (const commandMenuItemEntity of commandMenuItems) {
const flatCommandMenuItem =
fromCommandMenuItemEntityToFlatCommandMenuItem(commandMenuItemEntity);
fromCommandMenuItemEntityToFlatCommandMenuItem({
entity: commandMenuItemEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatCommandMenuItem,
@@ -1,9 +1,45 @@
import { type CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { isDefined } from 'twenty-shared/utils';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({
entity: commandMenuItemEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'commandMenuItem'>): FlatCommandMenuItem => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
commandMenuItemEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${commandMenuItemEntity.applicationId} not found for commandMenuItem ${commandMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let availabilityObjectMetadataUniversalIdentifier: string | null = null;
if (isDefined(commandMenuItemEntity.availabilityObjectMetadataId)) {
availabilityObjectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
commandMenuItemEntity.availabilityObjectMetadataId,
) ?? null;
if (!isDefined(availabilityObjectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ObjectMetadata with id ${commandMenuItemEntity.availabilityObjectMetadataId} not found for commandMenuItem ${commandMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
export const fromCommandMenuItemEntityToFlatCommandMenuItem = (
commandMenuItemEntity: CommandMenuItemEntity,
): FlatCommandMenuItem => {
return {
id: commandMenuItemEntity.id,
workflowVersionId: commandMenuItemEntity.workflowVersionId,
@@ -18,5 +54,10 @@ export const fromCommandMenuItemEntityToFlatCommandMenuItem = (
applicationId: commandMenuItemEntity.applicationId,
createdAt: commandMenuItemEntity.createdAt.toISOString(),
updatedAt: commandMenuItemEntity.updatedAt.toISOString(),
__universal: {
universalIdentifier: commandMenuItemEntity.universalIdentifier,
applicationUniversalIdentifier,
availabilityObjectMetadataUniversalIdentifier,
},
};
};
@@ -1,16 +1,13 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type Expect } from 'twenty-shared/testing';
import {
type ExtractPropertiesThatEndsWithId,
type ExtractPropertiesThatEndsWithIds,
} from 'twenty-shared/types';
import { type ExtractPropertiesThatEndsWithId } from 'twenty-shared/types';
import { type Relation } from 'typeorm';
import { type AddSuffixToEntityOneToManyProperties } from 'src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type';
import { type ExtractEntityManyToOneEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type';
import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type';
import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modules/flat-entity/types/from-metadata-entity-to-metadata-name.type';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
type ManyToOneRelationValue<
@@ -24,12 +21,12 @@ type ManyToOneRelationValue<
> extends Relation<infer TTargetEntity extends SyncableEntity>
? {
metadataName: FromMetadataEntityToMetadataName<TTargetEntity>;
flatEntityForeignKeyAggregator: ExtractPropertiesThatEndsWithIds<
MetadataFlatEntity<FromMetadataEntityToMetadataName<TTargetEntity>>
// Note: In the best of the world should not be nullable, entities should always declare inverside keys
> | null;
flatEntityForeignKeyAggregator:
| keyof AddSuffixToEntityOneToManyProperties<TTargetEntity, 'ids'>
| null;
// Note: In the best of the world should not be nullable, entities should always declare inverside keys
foreignKey: ExtractPropertiesThatEndsWithId<
MetadataFlatEntity<TSourceMetadataName>,
MetadataEntity<TSourceMetadataName>,
'id' | 'workspaceId'
>;
}
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { WorkspaceFlatFieldMetadataMapCacheService } from 'src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service';
@@ -22,6 +23,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
@@ -49,6 +51,8 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
PageLayoutWidgetEntity,
RowLevelPermissionPredicateEntity,
RowLevelPermissionPredicateGroupEntity,
ApplicationEntity,
RoleEntity,
]),
],
providers: [
@@ -0,0 +1,13 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type';
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type';
export type AddSuffixToEntityManyToOneProperties<
TEntity,
TMetadataName extends AllMetadataName,
TSuffix extends string,
> = {
[P in Extract<MetadataManyToOneJoinColumn<TMetadataName>, keyof TEntity> &
string as `${RemoveSuffix<P, 'Id'>}${Capitalize<TSuffix>}`]: TEntity[P];
};
@@ -0,0 +1,14 @@
import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type';
export type AddSuffixToEntityOneToManyProperties<
TEntity,
TSuffix extends string,
> = {
[P in ExtractEntityOneToManyEntityRelationProperties<
TEntity,
SyncableEntity
> &
string as `${RemoveSuffix<P, 's'>}${Capitalize<TSuffix>}`]: string[];
};
@@ -1,26 +1,42 @@
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type';
import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type';
import { type NonNullableProperties } from 'src/types/non-nullable-properties.type';
import { type AllMetadataName } from 'twenty-shared/metadata';
export type SyncableFlatEntity = NonNullableProperties<
Omit<SyncableEntity, 'application' | 'applicationId' | 'workspace'>
import { type AddSuffixToEntityOneToManyProperties } from 'src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type';
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type';
import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modules/flat-entity/types/from-metadata-entity-to-metadata-name.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { type UniversalFlatEntityExtraProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
export type SyncableFlatEntity = Omit<
SyncableEntity,
'application' | 'workspace'
> & {
id: string;
applicationId: string | null;
};
export type FlatEntityFrom<TEntity> = Omit<
export type FlatEntityFrom<
TEntity,
TMetadataName extends AllMetadataName | undefined = undefined,
> = Omit<
TEntity,
| ExtractEntityRelatedEntityProperties<TEntity>
| keyof CastRecordTypeOrmDatePropertiesToString<TEntity>
> &
CastRecordTypeOrmDatePropertiesToString<TEntity> & {
[P in ExtractEntityOneToManyEntityRelationProperties<
TEntity,
SyncableEntity
> &
string as `${RemoveSuffix<P, 's'>}Ids`]: string[];
};
CastRecordTypeOrmDatePropertiesToString<TEntity> &
AddSuffixToEntityOneToManyProperties<TEntity, 'ids'> &
(TEntity extends SyncableEntity
? {
/**
* /!\ Under migration the idea is at some point to replace FlatEntity by UniversalFlatEntity /!\
* Please avoid any usage or contact me ( prastoin ) before doing so
* TODO remove with FlatEntity once it has been fully migrated
*/
__universal?: UniversalFlatEntityExtraProperties<
TEntity,
TMetadataName extends undefined
? FromMetadataEntityToMetadataName<TEntity>
: TMetadataName
> & { universalIdentifier: string };
}
: // eslint-disable-next-line @typescript-eslint/no-empty-object-type
{});
@@ -45,6 +45,8 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
applicationId: faker.string.uuid(),
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
__universal: {} as any,
...overrides,
};
};
@@ -65,5 +65,6 @@ export const getRelationTargetFlatFieldMetadataMock = ({
defaultValue: null,
options: null,
applicationId: faker.string.uuid(),
__universal: undefined,
};
};
@@ -5,16 +5,19 @@ 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 { 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 { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { fromFieldMetadataEntityToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
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 { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
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 { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@@ -26,6 +29,10 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
constructor(
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(ViewFieldEntity)
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
@InjectRepository(ViewFilterEntity)
@@ -41,25 +48,43 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatFieldMetadata>> {
const [fieldMetadatas, viewFields, viewFilters, views] = await Promise.all([
const [
fieldMetadatas,
objectMetadatas,
applications,
viewFields,
viewFilters,
views,
] = await Promise.all([
this.fieldMetadataRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.objectMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.viewFieldRepository.find({
where: { workspaceId },
select: ['id', 'fieldMetadataId'],
select: ['id', 'universalIdentifier', 'fieldMetadataId'],
withDeleted: true,
}),
this.viewFilterRepository.find({
where: { workspaceId },
select: ['id', 'fieldMetadataId'],
select: ['id', 'universalIdentifier', 'fieldMetadataId'],
withDeleted: true,
}),
this.viewRepository.find({
where: { workspaceId },
select: [
'id',
'universalIdentifier',
'kanbanAggregateOperationFieldMetadataId',
'calendarFieldMetadataId',
'mainGroupByFieldMetadataId',
@@ -99,20 +124,34 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
] as const
).map(regroupEntitiesByRelatedEntityId);
const fieldMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(fieldMetadatas);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const flatFieldMetadataMaps = createEmptyFlatEntityMaps();
for (const fieldMetadataEntity of fieldMetadatas) {
const flatFieldMetadata = fromFieldMetadataEntityToFlatFieldMetadata({
...fieldMetadataEntity,
viewFields: viewFieldsByFieldId.get(fieldMetadataEntity.id) || [],
viewFilters: viewFiltersByFieldId.get(fieldMetadataEntity.id) || [],
kanbanAggregateOperationViews:
kanbanViewsByFieldId.get(fieldMetadataEntity.id) || [],
calendarViews: calendarViewsByFieldId.get(fieldMetadataEntity.id) || [],
mainGroupByFieldMetadataViews:
mainGroupByFieldMetadataViewsByFieldId.get(fieldMetadataEntity.id) ||
[],
} as FieldMetadataEntity);
entity: {
...fieldMetadataEntity,
viewFields: viewFieldsByFieldId.get(fieldMetadataEntity.id) || [],
viewFilters: viewFiltersByFieldId.get(fieldMetadataEntity.id) || [],
kanbanAggregateOperationViews:
kanbanViewsByFieldId.get(fieldMetadataEntity.id) || [],
calendarViews:
calendarViewsByFieldId.get(fieldMetadataEntity.id) || [],
mainGroupByFieldMetadataViews:
mainGroupByFieldMetadataViewsByFieldId.get(
fieldMetadataEntity.id,
) || [],
},
fieldMetadataIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
applicationIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatFieldMetadata,
@@ -8,5 +8,6 @@ export type FlatFieldMetadata<T extends FieldMetadataType = FieldMetadataType> =
Omit<
FieldMetadataEntity<T>,
'relationTargetFieldMetadata' | 'relationTargetObjectMetadata'
>
>,
'fieldMetadata'
>;
@@ -1,20 +1,97 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isFieldMetadataSettingsOfType } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-settings-of-type.util';
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 FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromFieldMetadataEntityToFlatFieldMetadata = <
T extends FieldMetadataType,
>(
fieldMetadataEntity: FieldMetadataEntity<T>,
// This is intended to be abstract
): FlatFieldMetadata => {
export const fromFieldMetadataEntityToFlatFieldMetadata = ({
entity: fieldMetadataEntity,
fieldMetadataIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
applicationIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'fieldMetadata'>): FlatFieldMetadata => {
const fieldMetadataWithoutRelations = removePropertiesFromRecord(
fieldMetadataEntity,
getMetadataEntityRelationProperties('fieldMetadata'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
fieldMetadataEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${fieldMetadataEntity.applicationId} not found when building flat field metadata for field ${fieldMetadataEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const objectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
fieldMetadataEntity.objectMetadataId,
);
if (!isDefined(objectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Object metadata with id ${fieldMetadataEntity.objectMetadataId} not found when building flat field metadata for field ${fieldMetadataEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let relationTargetObjectMetadataUniversalIdentifier: string | null = null;
if (isDefined(fieldMetadataEntity.relationTargetObjectMetadataId)) {
relationTargetObjectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
fieldMetadataEntity.relationTargetObjectMetadataId,
) ?? null;
if (!isDefined(relationTargetObjectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Relation target object metadata with id ${fieldMetadataEntity.relationTargetObjectMetadataId} not found when building flat field metadata for field ${fieldMetadataEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
let relationTargetFieldMetadataUniversalIdentifier: string | null = null;
if (isDefined(fieldMetadataEntity.relationTargetFieldMetadataId)) {
relationTargetFieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(
fieldMetadataEntity.relationTargetFieldMetadataId,
) ?? null;
if (!isDefined(relationTargetFieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Relation target field metadata with id ${fieldMetadataEntity.relationTargetFieldMetadataId} not found when building flat field metadata for field ${fieldMetadataEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
const settings = fieldMetadataEntity.settings;
const isRelationSettings =
isFieldMetadataSettingsOfType(settings, FieldMetadataType.RELATION) ||
isFieldMetadataSettingsOfType(settings, FieldMetadataType.MORPH_RELATION);
const settingsWithUniversalIdentifiers = isRelationSettings
? {
...settings,
...(isDefined(settings.junctionTargetFieldId) && {
junctionTargetFieldUniversalIdentifier:
fieldMetadataIdToUniversalIdentifierMap.get(
settings.junctionTargetFieldId,
),
}),
}
: settings;
return {
...fieldMetadataWithoutRelations,
@@ -29,5 +106,33 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
[],
viewFieldIds: fieldMetadataEntity.viewFields.map(({ id }) => id),
viewFilterIds: fieldMetadataEntity.viewFilters.map(({ id }) => id),
__universal: {
universalIdentifier: fieldMetadataWithoutRelations.universalIdentifier,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier,
relationTargetFieldMetadataUniversalIdentifier,
viewFieldUniversalIdentifiers: fieldMetadataEntity.viewFields.map(
({ universalIdentifier }) => universalIdentifier,
),
viewFilterUniversalIdentifiers: fieldMetadataEntity.viewFilters.map(
({ universalIdentifier }) => universalIdentifier,
),
kanbanAggregateOperationViewUniversalIdentifiers:
fieldMetadataEntity.kanbanAggregateOperationViews.map(
({ universalIdentifier }) => universalIdentifier,
),
calendarViewUniversalIdentifiers: fieldMetadataEntity.calendarViews.map(
({ universalIdentifier }) => universalIdentifier,
),
mainGroupByFieldMetadataViewUniversalIdentifiers:
fieldMetadataEntity.mainGroupByFieldMetadataViews?.map(
({ universalIdentifier }) => universalIdentifier,
) ?? [],
settings: settingsWithUniversalIdentifiers,
defaultValue: fieldMetadataWithoutRelations.defaultValue,
options: fieldMetadataWithoutRelations.options,
standardOverrides: fieldMetadataWithoutRelations.standardOverrides,
},
};
};
@@ -1,13 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceFlatFrontComponentMapCacheService } from 'src/engine/metadata-modules/flat-front-component/services/workspace-flat-front-component-map-cache.service';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
@Module({
imports: [
TypeOrmModule.forFeature([FrontComponentEntity]),
TypeOrmModule.forFeature([ApplicationEntity, FrontComponentEntity]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatFrontComponentMapCacheService],
@@ -5,11 +5,13 @@ 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 { type FlatFrontComponentMaps } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component-maps.type';
import { fromFrontComponentEntityToFlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/utils/from-front-component-entity-to-flat-front-component.util';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
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()
@@ -18,20 +20,34 @@ export class WorkspaceFlatFrontComponentMapCacheService extends WorkspaceCachePr
constructor(
@InjectRepository(FrontComponentEntity)
private readonly frontComponentRepository: Repository<FrontComponentEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatFrontComponentMaps> {
const frontComponents = await this.frontComponentRepository.find({
where: { workspaceId },
});
const [frontComponents, applications] = await Promise.all([
this.frontComponentRepository.find({
where: { workspaceId },
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const flatFrontComponentMaps = createEmptyFlatEntityMaps();
for (const frontComponentEntity of frontComponents) {
const flatFrontComponent =
fromFrontComponentEntityToFlatFrontComponent(frontComponentEntity);
const flatFrontComponent = fromFrontComponentEntityToFlatFrontComponent({
entity: frontComponentEntity,
applicationIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatFrontComponent,
@@ -1,9 +1,28 @@
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import { type FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromFrontComponentEntityToFlatFrontComponent = ({
entity: frontComponentEntity,
applicationIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'frontComponent'>): FlatFrontComponent => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
frontComponentEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${frontComponentEntity.applicationId} not found for frontComponent ${frontComponentEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
export const fromFrontComponentEntityToFlatFrontComponent = (
frontComponentEntity: FrontComponentEntity,
): FlatFrontComponent => {
return {
id: frontComponentEntity.id,
name: frontComponentEntity.name,
@@ -12,5 +31,9 @@ export const fromFrontComponentEntityToFlatFrontComponent = (
applicationId: frontComponentEntity.applicationId,
createdAt: frontComponentEntity.createdAt.toISOString(),
updatedAt: frontComponentEntity.updatedAt.toISOString(),
__universal: {
universalIdentifier: frontComponentEntity.universalIdentifier,
applicationUniversalIdentifier,
},
};
};
@@ -5,12 +5,15 @@ 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 { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { fromIndexMetadataEntityToFlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/utils/from-index-metadata-entity-to-flat-index-metadata.util';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
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 +24,10 @@ export class WorkspaceFlatIndexMapCacheService extends WorkspaceCacheProvider<
constructor(
@InjectRepository(IndexMetadataEntity)
private readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {
super();
}
@@ -28,23 +35,43 @@ export class WorkspaceFlatIndexMapCacheService extends WorkspaceCacheProvider<
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatIndexMetadata>> {
const indexes = await this.indexMetadataRepository.find({
where: {
workspaceId,
},
withDeleted: true,
relationLoadStrategy: 'join',
select: {
// Note: We need all IndexFieldMetadataEntity in order to build a FlatIndex
indexFieldMetadatas: true,
},
relations: ['indexFieldMetadatas'],
});
const [indexes, applications, objectMetadatas] = await Promise.all([
this.indexMetadataRepository.find({
where: {
workspaceId,
},
withDeleted: true,
relationLoadStrategy: 'join',
select: {
// Note: We need all IndexFieldMetadataEntity in order to build a FlatIndex
indexFieldMetadatas: true,
},
relations: ['indexFieldMetadatas'],
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
}),
this.objectMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);
const flatIndexMaps = createEmptyFlatEntityMaps();
for (const indexEntity of indexes) {
const flatIndex = fromIndexMetadataEntityToFlatIndexMetadata(indexEntity);
const flatIndex = fromIndexMetadataEntityToFlatIndexMetadata({
entity: indexEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatIndex,
@@ -1,12 +1,42 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
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 FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromIndexMetadataEntityToFlatIndexMetadata = ({
entity: indexMetadataEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'index'>): FlatIndexMetadata => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
indexMetadataEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${indexMetadataEntity.applicationId} not found for index ${indexMetadataEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const objectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
indexMetadataEntity.objectMetadataId,
);
if (!isDefined(objectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ObjectMetadata with id ${indexMetadataEntity.objectMetadataId} not found for index ${indexMetadataEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
export const fromIndexMetadataEntityToFlatIndexMetadata = (
indexMetadataEntity: IndexMetadataEntity,
): FlatIndexMetadata => {
const indexMetadataEntityWithoutRelations = removePropertiesFromRecord(
indexMetadataEntity,
getMetadataEntityRelationProperties('index'),
@@ -28,5 +58,10 @@ export const fromIndexMetadataEntityToFlatIndexMetadata = (
updatedAt: indexFieldMetadata.updatedAt.toISOString(),
}),
),
__universal: {
universalIdentifier: indexMetadataEntity.universalIdentifier,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
},
};
};
@@ -1,13 +1,21 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceFlatNavigationMenuItemMapCacheService } from 'src/engine/metadata-modules/flat-navigation-menu-item/services/workspace-flat-navigation-menu-item-map-cache.service';
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
@Module({
imports: [
TypeOrmModule.forFeature([NavigationMenuItemEntity]),
TypeOrmModule.forFeature([
NavigationMenuItemEntity,
ApplicationEntity,
ObjectMetadataEntity,
ViewEntity,
]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatNavigationMenuItemMapCacheService],
@@ -5,12 +5,16 @@ 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 { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
import { addFlatNavigationMenuItemToMapsAndUpdateIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/add-flat-navigation-menu-item-to-maps-and-update-index.util';
import { fromNavigationMenuItemEntityToFlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-navigation-menu-item-entity-to-flat-navigation-menu-item.util';
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
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';
@Injectable()
@WorkspaceCache('flatNavigationMenuItemMaps')
@@ -18,6 +22,12 @@ export class WorkspaceFlatNavigationMenuItemMapCacheService extends WorkspaceCac
constructor(
@InjectRepository(NavigationMenuItemEntity)
private readonly navigationMenuItemRepository: Repository<NavigationMenuItemEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
) {
super();
}
@@ -25,10 +35,37 @@ export class WorkspaceFlatNavigationMenuItemMapCacheService extends WorkspaceCac
async computeForCache(
workspaceId: string,
): Promise<FlatNavigationMenuItemMaps> {
const navigationMenuItems = await this.navigationMenuItemRepository.find({
where: { workspaceId },
withDeleted: true,
});
const [navigationMenuItems, applications, objectMetadatas, views] =
await Promise.all([
this.navigationMenuItemRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.objectMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.viewRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);
const navigationMenuItemIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(navigationMenuItems);
const viewIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(views);
const flatNavigationMenuItemMaps = {
...createEmptyFlatEntityMaps(),
@@ -37,9 +74,13 @@ export class WorkspaceFlatNavigationMenuItemMapCacheService extends WorkspaceCac
for (const navigationMenuItemEntity of navigationMenuItems) {
const flatNavigationMenuItem =
fromNavigationMenuItemEntityToFlatNavigationMenuItem(
navigationMenuItemEntity,
);
fromNavigationMenuItemEntityToFlatNavigationMenuItem({
entity: navigationMenuItemEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
navigationMenuItemIdToUniversalIdentifierMap,
viewIdToUniversalIdentifierMap,
});
addFlatNavigationMenuItemToMapsAndUpdateIndex({
flatNavigationMenuItem,
@@ -1,9 +1,78 @@
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
import { type NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = ({
entity: navigationMenuItemEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
navigationMenuItemIdToUniversalIdentifierMap,
viewIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'navigationMenuItem'>): FlatNavigationMenuItem => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
navigationMenuItemEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${navigationMenuItemEntity.applicationId} not found for navigationMenuItem ${navigationMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let targetObjectMetadataUniversalIdentifier: string | null = null;
if (isDefined(navigationMenuItemEntity.targetObjectMetadataId)) {
targetObjectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
navigationMenuItemEntity.targetObjectMetadataId,
) ?? null;
if (!isDefined(targetObjectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ObjectMetadata with id ${navigationMenuItemEntity.targetObjectMetadataId} not found for navigationMenuItem ${navigationMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
let folderUniversalIdentifier: string | null = null;
if (isDefined(navigationMenuItemEntity.folderId)) {
folderUniversalIdentifier =
navigationMenuItemIdToUniversalIdentifierMap.get(
navigationMenuItemEntity.folderId,
) ?? null;
if (!isDefined(folderUniversalIdentifier)) {
throw new FlatEntityMapsException(
`NavigationMenuItem (folder) with id ${navigationMenuItemEntity.folderId} not found for navigationMenuItem ${navigationMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
let viewUniversalIdentifier: string | null = null;
if (isDefined(navigationMenuItemEntity.viewId)) {
viewUniversalIdentifier =
viewIdToUniversalIdentifierMap.get(navigationMenuItemEntity.viewId) ??
null;
if (!isDefined(viewUniversalIdentifier)) {
throw new FlatEntityMapsException(
`View with id ${navigationMenuItemEntity.viewId} not found for navigationMenuItem ${navigationMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = (
navigationMenuItemEntity: NavigationMenuItemEntity,
): FlatNavigationMenuItem => {
return {
id: navigationMenuItemEntity.id,
userWorkspaceId: navigationMenuItemEntity.userWorkspaceId,
@@ -18,5 +87,12 @@ export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = (
applicationId: navigationMenuItemEntity.applicationId,
createdAt: navigationMenuItemEntity.createdAt.toISOString(),
updatedAt: navigationMenuItemEntity.updatedAt.toISOString(),
__universal: {
universalIdentifier: navigationMenuItemEntity.universalIdentifier,
applicationUniversalIdentifier,
targetObjectMetadataUniversalIdentifier,
folderUniversalIdentifier,
viewUniversalIdentifier,
},
};
};
@@ -5,6 +5,7 @@ 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 { 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 { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
@@ -14,6 +15,7 @@ import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
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 { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@@ -25,6 +27,8 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
constructor(
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(IndexMetadataEntity)
@@ -38,27 +42,33 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatObjectMetadata>> {
const [objectMetadatas, fields, indexMetadatas, views] = await Promise.all([
this.objectMetadataRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id', 'objectMetadataId'],
withDeleted: true,
}),
this.indexMetadataRepository.find({
where: { workspaceId },
select: ['id', 'objectMetadataId'],
withDeleted: true,
}),
this.viewRepository.find({
where: { workspaceId },
select: ['id', 'objectMetadataId'],
withDeleted: true,
}),
]);
const [objectMetadatas, applications, fields, indexMetadatas, views] =
await Promise.all([
this.objectMetadataRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'objectMetadataId'],
withDeleted: true,
}),
this.indexMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'objectMetadataId'],
withDeleted: true,
}),
this.viewRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'objectMetadataId'],
withDeleted: true,
}),
]);
const [fieldsByObjectId, indexesByObjectId, viewsByObjectId] = (
[
@@ -77,15 +87,24 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
] as const
).map(regroupEntitiesByRelatedEntityId);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const fieldMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(fields);
const flatObjectMetadataMaps = createEmptyFlatEntityMaps();
for (const objectMetadataEntity of objectMetadatas) {
const flatObjectMetadata = fromObjectMetadataEntityToFlatObjectMetadata({
...objectMetadataEntity,
fields: fieldsByObjectId.get(objectMetadataEntity.id) || [],
indexMetadatas: indexesByObjectId.get(objectMetadataEntity.id) || [],
views: viewsByObjectId.get(objectMetadataEntity.id) || [],
} as ObjectMetadataEntity);
entity: {
...objectMetadataEntity,
fields: fieldsByObjectId.get(objectMetadataEntity.id) || [],
indexMetadatas: indexesByObjectId.get(objectMetadataEntity.id) || [],
views: viewsByObjectId.get(objectMetadataEntity.id) || [],
},
applicationIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatObjectMetadata,
@@ -1,6 +1,13 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
export type FlatObjectMetadata = FlatEntityFrom<
type BaseFlatObjectMetadata = FlatEntityFrom<
Omit<ObjectMetadataEntity, 'targetRelationFields' | 'dataSourceId'>
>;
export type FlatObjectMetadata = Omit<BaseFlatObjectMetadata, '__universal'> & {
__universal?: BaseFlatObjectMetadata['__universal'] & {
// TODO remove once https://github.com/twentyhq/core-team-issues/issues/2172 has been resolved
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
imageIdentifierFieldMetadataUniversalIdentifier: string | null;
};
};
@@ -1,27 +1,99 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
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 FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromObjectMetadataEntityToFlatObjectMetadata = (
objectMetadataEntity: ObjectMetadataEntity,
): FlatObjectMetadata => {
type FromObjectMetadataEntityToFlatObjectMetadataArgs =
FromEntityToFlatEntityArgs<'objectMetadata'> & {
fieldMetadataIdToUniversalIdentifierMap: Map<string, string>;
};
export const fromObjectMetadataEntityToFlatObjectMetadata = ({
entity: objectMetadataEntity,
applicationIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
}: FromObjectMetadataEntityToFlatObjectMetadataArgs): FlatObjectMetadata => {
const objectMetadataEntityWithoutRelations = removePropertiesFromRecord(
objectMetadataEntity,
getMetadataEntityRelationProperties('objectMetadata'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
objectMetadataEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${objectMetadataEntity.applicationId} not found when building flat object metadata for object ${objectMetadataEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let labelIdentifierFieldMetadataUniversalIdentifier: string | null = null;
if (isDefined(objectMetadataEntity.labelIdentifierFieldMetadataId)) {
labelIdentifierFieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(
objectMetadataEntity.labelIdentifierFieldMetadataId,
) ?? null;
if (!isDefined(labelIdentifierFieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Label identifier field metadata with id ${objectMetadataEntity.labelIdentifierFieldMetadataId} not found when building flat object metadata for object ${objectMetadataEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
let imageIdentifierFieldMetadataUniversalIdentifier: string | null = null;
if (isDefined(objectMetadataEntity.imageIdentifierFieldMetadataId)) {
imageIdentifierFieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(
objectMetadataEntity.imageIdentifierFieldMetadataId,
) ?? null;
if (!isDefined(imageIdentifierFieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Image identifier field metadata with id ${objectMetadataEntity.imageIdentifierFieldMetadataId} not found when building flat object metadata for object ${objectMetadataEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
return {
...objectMetadataEntityWithoutRelations,
universalIdentifier:
objectMetadataEntityWithoutRelations.universalIdentifier,
createdAt: objectMetadataEntity.createdAt.toISOString(),
updatedAt: objectMetadataEntity.updatedAt.toISOString(),
viewIds: objectMetadataEntity.views.map((viewEntity) => viewEntity.id),
indexMetadataIds: objectMetadataEntity.indexMetadatas.map(
(indexEntity) => indexEntity.id,
),
fieldIds: objectMetadataEntity.fields.map((fieldEntity) => fieldEntity.id),
viewIds: objectMetadataEntity.views.map(({ id }) => id),
indexMetadataIds: objectMetadataEntity.indexMetadatas.map(({ id }) => id),
fieldIds: objectMetadataEntity.fields.map(({ id }) => id),
__universal: {
universalIdentifier:
objectMetadataEntityWithoutRelations.universalIdentifier,
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
imageIdentifierFieldMetadataUniversalIdentifier,
fieldUniversalIdentifiers: objectMetadataEntity.fields.map(
({ universalIdentifier }) => universalIdentifier,
),
indexMetadataUniversalIdentifiers:
objectMetadataEntity.indexMetadatas.map(
({ universalIdentifier }) => universalIdentifier,
),
viewUniversalIdentifiers: objectMetadataEntity.views.map(
({ universalIdentifier }) => universalIdentifier,
),
standardOverrides: objectMetadataEntityWithoutRelations.standardOverrides,
duplicateCriteria: objectMetadataEntityWithoutRelations.duplicateCriteria,
},
};
};
@@ -1,14 +1,21 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceFlatPageLayoutTabMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-tab/services/workspace-flat-page-layout-tab-map-cache.service';
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
@Module({
imports: [
TypeOrmModule.forFeature([PageLayoutTabEntity, PageLayoutWidgetEntity]),
TypeOrmModule.forFeature([
PageLayoutTabEntity,
PageLayoutWidgetEntity,
ApplicationEntity,
PageLayoutEntity,
]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatPageLayoutTabMapCacheService],
@@ -5,12 +5,15 @@ 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 { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
import { transformPageLayoutTabEntityToFlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util';
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
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 { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@@ -22,21 +25,37 @@ export class WorkspaceFlatPageLayoutTabMapCacheService extends WorkspaceCachePro
private readonly pageLayoutTabRepository: Repository<PageLayoutTabEntity>,
@InjectRepository(PageLayoutWidgetEntity)
private readonly pageLayoutWidgetRepository: Repository<PageLayoutWidgetEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(PageLayoutEntity)
private readonly pageLayoutRepository: Repository<PageLayoutEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatPageLayoutTabMaps> {
const [pageLayoutTabs, pageLayoutWidgets] = await Promise.all([
await this.pageLayoutTabRepository.find({
where: { workspaceId },
withDeleted: true,
}),
await this.pageLayoutWidgetRepository.find({
where: { workspaceId },
withDeleted: true,
}),
]);
const [pageLayoutTabs, pageLayoutWidgets, applications, pageLayouts] =
await Promise.all([
this.pageLayoutTabRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.pageLayoutWidgetRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'pageLayoutTabId'],
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.pageLayoutRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const [pageLayoutWidgetsByPageLayoutTabId] = (
[
@@ -47,16 +66,25 @@ export class WorkspaceFlatPageLayoutTabMapCacheService extends WorkspaceCachePro
] as const
).map(regroupEntitiesByRelatedEntityId);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const pageLayoutIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(pageLayouts);
const flatPageLayoutTabMaps = createEmptyFlatEntityMaps();
for (const pageLayoutTabEntity of pageLayoutTabs) {
const flatPageLayoutTab = transformPageLayoutTabEntityToFlatPageLayoutTab(
{
...pageLayoutTabEntity,
widgets:
pageLayoutWidgetsByPageLayoutTabId.get(pageLayoutTabEntity.id) ||
[],
} as PageLayoutTabEntity,
entity: {
...pageLayoutTabEntity,
widgets:
pageLayoutWidgetsByPageLayoutTabId.get(pageLayoutTabEntity.id) ||
[],
},
applicationIdToUniversalIdentifierMap,
pageLayoutIdToUniversalIdentifierMap,
},
);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
@@ -1,9 +1,39 @@
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { type PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const transformPageLayoutTabEntityToFlatPageLayoutTab = ({
entity: pageLayoutTabEntity,
applicationIdToUniversalIdentifierMap,
pageLayoutIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'pageLayoutTab'>): FlatPageLayoutTab => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
pageLayoutTabEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${pageLayoutTabEntity.applicationId} not found for pageLayoutTab ${pageLayoutTabEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const pageLayoutUniversalIdentifier =
pageLayoutIdToUniversalIdentifierMap.get(pageLayoutTabEntity.pageLayoutId);
if (!isDefined(pageLayoutUniversalIdentifier)) {
throw new FlatEntityMapsException(
`PageLayout with id ${pageLayoutTabEntity.pageLayoutId} not found for pageLayoutTab ${pageLayoutTabEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
export const transformPageLayoutTabEntityToFlatPageLayoutTab = (
pageLayoutTabEntity: PageLayoutTabEntity,
): FlatPageLayoutTab => {
return {
createdAt: pageLayoutTabEntity.createdAt.toISOString(),
deletedAt: pageLayoutTabEntity.deletedAt?.toISOString() ?? null,
@@ -16,5 +46,13 @@ export const transformPageLayoutTabEntityToFlatPageLayoutTab = (
universalIdentifier: pageLayoutTabEntity.universalIdentifier,
applicationId: pageLayoutTabEntity.applicationId,
widgetIds: pageLayoutTabEntity.widgets.map((widget) => widget.id),
__universal: {
universalIdentifier: pageLayoutTabEntity.universalIdentifier,
applicationUniversalIdentifier,
pageLayoutUniversalIdentifier,
widgetUniversalIdentifiers: pageLayoutTabEntity.widgets.map(
(widget) => widget.universalIdentifier,
),
},
};
};
@@ -1,12 +1,24 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { WorkspaceFlatPageLayoutWidgetMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
@Module({
imports: [TypeOrmModule.forFeature([PageLayoutWidgetEntity])],
imports: [
TypeOrmModule.forFeature([
PageLayoutWidgetEntity,
ApplicationEntity,
PageLayoutTabEntity,
ObjectMetadataEntity,
FieldMetadataEntity,
]),
],
providers: [
WorkspaceFlatPageLayoutWidgetMapCacheService,
FlatPageLayoutWidgetTypeValidatorService,
@@ -5,11 +5,16 @@ 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 { 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 { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
import { fromPageLayoutWidgetEntityToFlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
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()
@@ -18,6 +23,14 @@ export class WorkspaceFlatPageLayoutWidgetMapCacheService extends WorkspaceCache
constructor(
@InjectRepository(PageLayoutWidgetEntity)
private readonly pageLayoutWidgetRepository: Repository<PageLayoutWidgetEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(PageLayoutTabEntity)
private readonly pageLayoutTabRepository: Repository<PageLayoutTabEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
) {
super();
}
@@ -25,21 +38,59 @@ export class WorkspaceFlatPageLayoutWidgetMapCacheService extends WorkspaceCache
async computeForCache(
workspaceId: string,
): Promise<FlatPageLayoutWidgetMaps> {
const existingPageLayoutWidgets =
await this.pageLayoutWidgetRepository.find({
where: {
workspaceId,
},
const [
existingPageLayoutWidgets,
applications,
pageLayoutTabs,
objectMetadatas,
fieldMetadatas,
] = await Promise.all([
this.pageLayoutWidgetRepository.find({
where: { workspaceId },
withDeleted: true,
});
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.pageLayoutTabRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.objectMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const pageLayoutTabIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(pageLayoutTabs);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);
const fieldMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(fieldMetadatas);
const flatPageLayoutWidgetMaps = createEmptyFlatEntityMaps();
for (const pageLayoutWidgetEntity of existingPageLayoutWidgets) {
const flatPageLayoutWidget =
fromPageLayoutWidgetEntityToFlatPageLayoutWidget(
pageLayoutWidgetEntity,
);
fromPageLayoutWidgetEntityToFlatPageLayoutWidget({
entity: pageLayoutWidgetEntity,
applicationIdToUniversalIdentifierMap,
pageLayoutTabIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatPageLayoutWidget,
@@ -0,0 +1,166 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
type PageLayoutWidgetConfiguration = PageLayoutWidgetEntity['configuration'];
type UniversalPageLayoutWidgetConfiguration = NonNullable<
FlatPageLayoutWidget['__universal']
>['configuration'];
// Field metadata IDs in widget configurations don't need to reference existing entities
const getFieldMetadataUniversalIdentifier = ({
fieldMetadataId,
fieldMetadataIdToUniversalIdentifierMap,
}: {
fieldMetadataId: string;
fieldMetadataIdToUniversalIdentifierMap: Map<string, string>;
}): string | null => {
return fieldMetadataIdToUniversalIdentifierMap.get(fieldMetadataId) ?? null;
};
export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({
configuration,
fieldMetadataIdToUniversalIdentifierMap,
}: {
configuration: PageLayoutWidgetConfiguration;
fieldMetadataIdToUniversalIdentifierMap: Map<string, string>;
}): UniversalPageLayoutWidgetConfiguration => {
switch (configuration.configurationType) {
case WidgetConfigurationType.AGGREGATE_CHART: {
const { aggregateFieldMetadataId, ratioAggregateConfig, ...rest } =
configuration;
return {
...rest,
aggregateFieldMetadataUniversalIdentifier:
getFieldMetadataUniversalIdentifier({
fieldMetadataId: aggregateFieldMetadataId,
fieldMetadataIdToUniversalIdentifierMap,
}),
...(isDefined(ratioAggregateConfig) && {
ratioAggregateConfig: {
optionValue: ratioAggregateConfig.optionValue,
fieldMetadataUniversalIdentifier:
getFieldMetadataUniversalIdentifier({
fieldMetadataId: ratioAggregateConfig.fieldMetadataId,
fieldMetadataIdToUniversalIdentifierMap,
}),
},
}),
};
}
case WidgetConfigurationType.GAUGE_CHART: {
const { aggregateFieldMetadataId, ...rest } = configuration;
return {
...rest,
aggregateFieldMetadataUniversalIdentifier:
getFieldMetadataUniversalIdentifier({
fieldMetadataId: aggregateFieldMetadataId,
fieldMetadataIdToUniversalIdentifierMap,
}),
};
}
case WidgetConfigurationType.PIE_CHART: {
const { aggregateFieldMetadataId, groupByFieldMetadataId, ...rest } =
configuration;
return {
...rest,
aggregateFieldMetadataUniversalIdentifier:
getFieldMetadataUniversalIdentifier({
fieldMetadataId: aggregateFieldMetadataId,
fieldMetadataIdToUniversalIdentifierMap,
}),
groupByFieldMetadataUniversalIdentifier:
getFieldMetadataUniversalIdentifier({
fieldMetadataId: groupByFieldMetadataId,
fieldMetadataIdToUniversalIdentifierMap,
}),
};
}
case WidgetConfigurationType.BAR_CHART: {
const {
aggregateFieldMetadataId,
primaryAxisGroupByFieldMetadataId,
secondaryAxisGroupByFieldMetadataId,
...rest
} = configuration;
return {
...rest,
aggregateFieldMetadataUniversalIdentifier:
getFieldMetadataUniversalIdentifier({
fieldMetadataId: aggregateFieldMetadataId,
fieldMetadataIdToUniversalIdentifierMap,
}),
primaryAxisGroupByFieldMetadataUniversalIdentifier:
getFieldMetadataUniversalIdentifier({
fieldMetadataId: primaryAxisGroupByFieldMetadataId,
fieldMetadataIdToUniversalIdentifierMap,
}),
...(isDefined(secondaryAxisGroupByFieldMetadataId) && {
secondaryAxisGroupByFieldMetadataUniversalIdentifier:
getFieldMetadataUniversalIdentifier({
fieldMetadataId: secondaryAxisGroupByFieldMetadataId,
fieldMetadataIdToUniversalIdentifierMap,
}),
}),
};
}
case WidgetConfigurationType.LINE_CHART: {
const {
aggregateFieldMetadataId,
primaryAxisGroupByFieldMetadataId,
secondaryAxisGroupByFieldMetadataId,
...rest
} = configuration;
return {
...rest,
aggregateFieldMetadataUniversalIdentifier:
getFieldMetadataUniversalIdentifier({
fieldMetadataId: aggregateFieldMetadataId,
fieldMetadataIdToUniversalIdentifierMap,
}),
primaryAxisGroupByFieldMetadataUniversalIdentifier:
getFieldMetadataUniversalIdentifier({
fieldMetadataId: primaryAxisGroupByFieldMetadataId,
fieldMetadataIdToUniversalIdentifierMap,
}),
...(isDefined(secondaryAxisGroupByFieldMetadataId) && {
secondaryAxisGroupByFieldMetadataUniversalIdentifier:
getFieldMetadataUniversalIdentifier({
fieldMetadataId: secondaryAxisGroupByFieldMetadataId,
fieldMetadataIdToUniversalIdentifierMap,
}),
}),
};
}
case WidgetConfigurationType.VIEW:
case WidgetConfigurationType.FIELD:
case WidgetConfigurationType.FIELDS:
case WidgetConfigurationType.TIMELINE:
case WidgetConfigurationType.TASKS:
case WidgetConfigurationType.NOTES:
case WidgetConfigurationType.FILES:
case WidgetConfigurationType.EMAILS:
case WidgetConfigurationType.CALENDAR:
case WidgetConfigurationType.FIELD_RICH_TEXT:
case WidgetConfigurationType.WORKFLOW:
case WidgetConfigurationType.WORKFLOW_VERSION:
case WidgetConfigurationType.WORKFLOW_RUN:
case WidgetConfigurationType.FRONT_COMPONENT:
case WidgetConfigurationType.IFRAME:
case WidgetConfigurationType.STANDALONE_RICH_TEXT:
return configuration;
}
};
@@ -1,17 +1,77 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
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 FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { fromPageLayoutWidgetConfigurationToUniversalConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = (
pageLayoutWidgetEntity: PageLayoutWidgetEntity,
): FlatPageLayoutWidget => {
type FromPageLayoutWidgetEntityToFlatPageLayoutWidgetArgs =
FromEntityToFlatEntityArgs<'pageLayoutWidget'> & {
fieldMetadataIdToUniversalIdentifierMap: Map<string, string>;
};
export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({
entity: pageLayoutWidgetEntity,
applicationIdToUniversalIdentifierMap,
pageLayoutTabIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
}: FromPageLayoutWidgetEntityToFlatPageLayoutWidgetArgs): FlatPageLayoutWidget => {
const pageLayoutWidgetEntityWithoutRelations = removePropertiesFromRecord(
pageLayoutWidgetEntity,
getMetadataEntityRelationProperties('pageLayoutWidget'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
pageLayoutWidgetEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${pageLayoutWidgetEntity.applicationId} not found for pageLayoutWidget ${pageLayoutWidgetEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const pageLayoutTabUniversalIdentifier =
pageLayoutTabIdToUniversalIdentifierMap.get(
pageLayoutWidgetEntity.pageLayoutTabId,
);
if (!isDefined(pageLayoutTabUniversalIdentifier)) {
throw new FlatEntityMapsException(
`PageLayoutTab with id ${pageLayoutWidgetEntity.pageLayoutTabId} not found for pageLayoutWidget ${pageLayoutWidgetEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let objectMetadataUniversalIdentifier: string | null = null;
if (isDefined(pageLayoutWidgetEntity.objectMetadataId)) {
objectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
pageLayoutWidgetEntity.objectMetadataId,
) ?? null;
if (!isDefined(objectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ObjectMetadata with id ${pageLayoutWidgetEntity.objectMetadataId} not found for pageLayoutWidget ${pageLayoutWidgetEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
const configurationWithUniversalIdentifiers =
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
configuration: pageLayoutWidgetEntityWithoutRelations.configuration,
fieldMetadataIdToUniversalIdentifierMap,
});
return {
...pageLayoutWidgetEntityWithoutRelations,
createdAt: pageLayoutWidgetEntity.createdAt.toISOString(),
@@ -20,5 +80,13 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = (
universalIdentifier:
pageLayoutWidgetEntityWithoutRelations.universalIdentifier,
applicationId: pageLayoutWidgetEntityWithoutRelations.applicationId,
__universal: {
universalIdentifier: pageLayoutWidgetEntity.universalIdentifier,
applicationUniversalIdentifier,
pageLayoutTabUniversalIdentifier,
objectMetadataUniversalIdentifier,
gridPosition: pageLayoutWidgetEntityWithoutRelations.gridPosition,
configuration: configurationWithUniversalIdentifiers,
},
};
};
@@ -1,12 +1,21 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceFlatPageLayoutMapCacheService } from 'src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
@Module({
imports: [TypeOrmModule.forFeature([PageLayoutEntity, PageLayoutTabEntity])],
imports: [
TypeOrmModule.forFeature([
PageLayoutEntity,
PageLayoutTabEntity,
ApplicationEntity,
ObjectMetadataEntity,
]),
],
providers: [WorkspaceFlatPageLayoutMapCacheService],
exports: [WorkspaceFlatPageLayoutMapCacheService],
})
@@ -5,12 +5,15 @@ 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 { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout-maps.type';
import { transformPageLayoutEntityToFlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/utils/transform-page-layout-entity-to-flat-page-layout.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
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 { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@@ -22,21 +25,37 @@ export class WorkspaceFlatPageLayoutMapCacheService extends WorkspaceCacheProvid
private readonly pageLayoutRepository: Repository<PageLayoutEntity>,
@InjectRepository(PageLayoutTabEntity)
private readonly pageLayoutTabRepository: Repository<PageLayoutTabEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatPageLayoutMaps> {
const [pageLayouts, pageLayoutTabs] = await Promise.all([
await this.pageLayoutRepository.find({
where: { workspaceId },
withDeleted: true,
}),
await this.pageLayoutTabRepository.find({
where: { workspaceId },
withDeleted: true,
}),
]);
const [pageLayouts, pageLayoutTabs, applications, objectMetadatas] =
await Promise.all([
this.pageLayoutRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.pageLayoutTabRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'pageLayoutId'],
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.objectMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const [pageLayoutTabsByPageLayoutId] = (
[
@@ -47,13 +66,22 @@ export class WorkspaceFlatPageLayoutMapCacheService extends WorkspaceCacheProvid
] as const
).map(regroupEntitiesByRelatedEntityId);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);
const flatPageLayoutMaps = createEmptyFlatEntityMaps();
for (const pageLayoutEntity of pageLayouts) {
const flatPageLayout = transformPageLayoutEntityToFlatPageLayout({
...pageLayoutEntity,
tabs: pageLayoutTabsByPageLayoutId.get(pageLayoutEntity.id) || [],
} as PageLayoutEntity);
entity: {
...pageLayoutEntity,
tabs: pageLayoutTabsByPageLayoutId.get(pageLayoutEntity.id) || [],
},
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatPageLayout,
@@ -1,9 +1,43 @@
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { type PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const transformPageLayoutEntityToFlatPageLayout = ({
entity: pageLayoutEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'pageLayout'>): FlatPageLayout => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(pageLayoutEntity.applicationId);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${pageLayoutEntity.applicationId} not found for pageLayout ${pageLayoutEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let objectMetadataUniversalIdentifier: string | null = null;
if (isDefined(pageLayoutEntity.objectMetadataId)) {
objectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
pageLayoutEntity.objectMetadataId,
) ?? null;
if (!isDefined(objectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ObjectMetadata with id ${pageLayoutEntity.objectMetadataId} not found for pageLayout ${pageLayoutEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
export const transformPageLayoutEntityToFlatPageLayout = (
pageLayoutEntity: PageLayoutEntity,
): FlatPageLayout => {
return {
createdAt: pageLayoutEntity.createdAt.toISOString(),
deletedAt: pageLayoutEntity.deletedAt?.toISOString() ?? null,
@@ -16,5 +50,13 @@ export const transformPageLayoutEntityToFlatPageLayout = (
universalIdentifier: pageLayoutEntity.universalIdentifier,
applicationId: pageLayoutEntity.applicationId,
tabIds: pageLayoutEntity.tabs.map((tab) => tab.id),
__universal: {
universalIdentifier: pageLayoutEntity.universalIdentifier,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
tabUniversalIdentifiers: pageLayoutEntity.tabs.map(
(tab) => tab.universalIdentifier,
),
},
};
};
@@ -5,11 +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 { type FlatRoleTargetMaps } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type';
import { fromRoleTargetsEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util';
import { fromRoleTargetEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
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()
@@ -18,23 +21,45 @@ export class WorkspaceFlatRoleTargetMapCacheService extends WorkspaceCacheProvid
constructor(
@InjectRepository(RoleTargetEntity)
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatRoleTargetMaps> {
const roleTargets = await this.roleTargetRepository.find({
where: {
workspaceId,
},
withDeleted: true,
});
const [roleTargets, applications, roles] = await Promise.all([
this.roleTargetRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.roleRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);
const flatRoleTargetMaps = createEmptyFlatEntityMaps();
for (const roleTargetEntity of roleTargets) {
const flatRoleTarget =
fromRoleTargetsEntityToFlatRoleTarget(roleTargetEntity);
const flatRoleTarget = fromRoleTargetEntityToFlatRoleTarget({
entity: roleTargetEntity,
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatRoleTarget,
@@ -1,19 +1,53 @@
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { type RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromRoleTargetEntityToFlatRoleTarget = ({
entity: roleTargetEntity,
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'roleTarget'>): FlatRoleTarget => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(roleTargetEntity.applicationId);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${roleTargetEntity.applicationId} not found when building flat role target for role target ${roleTargetEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
roleTargetEntity.roleId,
);
if (!isDefined(roleUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Role with id ${roleTargetEntity.roleId} not found when building flat role target for role target ${roleTargetEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
export const fromRoleTargetsEntityToFlatRoleTarget = (
roleTarget: RoleTargetEntity,
): FlatRoleTarget => {
return {
id: roleTarget.id,
workspaceId: roleTarget.workspaceId,
roleId: roleTarget.roleId,
userWorkspaceId: roleTarget.userWorkspaceId,
agentId: roleTarget.agentId,
apiKeyId: roleTarget.apiKeyId,
applicationId: roleTarget.applicationId,
universalIdentifier: roleTarget.universalIdentifier,
createdAt: roleTarget.createdAt.toISOString(),
updatedAt: roleTarget.updatedAt.toISOString(),
id: roleTargetEntity.id,
workspaceId: roleTargetEntity.workspaceId,
roleId: roleTargetEntity.roleId,
userWorkspaceId: roleTargetEntity.userWorkspaceId,
agentId: roleTargetEntity.agentId,
apiKeyId: roleTargetEntity.apiKeyId,
applicationId: roleTargetEntity.applicationId,
universalIdentifier: roleTargetEntity.universalIdentifier,
createdAt: roleTargetEntity.createdAt.toISOString(),
updatedAt: roleTargetEntity.updatedAt.toISOString(),
__universal: {
universalIdentifier: roleTargetEntity.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
},
};
};
@@ -1,36 +1,84 @@
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { type EntityManyToOneIdByUniversalIdentifierMaps } from 'src/engine/workspace-cache/types/entity-many-to-one-id-by-universal-identifier-maps.type';
import { type EntityWithRegroupedOneToManyRelations } from 'src/engine/workspace-cache/types/entity-with-regrouped-one-to-many-relations.type';
import { type RegroupedEntity } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
type FromRoleEntityToFlatRoleArgs = {
entity: Omit<
EntityWithRegroupedOneToManyRelations<MetadataEntity<'role'>>,
'objectPermissions' | 'permissionFlags' | 'fieldPermissions'
> & {
objectPermissions: RegroupedEntity[];
permissionFlags: RegroupedEntity[];
fieldPermissions: RegroupedEntity[];
};
} & EntityManyToOneIdByUniversalIdentifierMaps<'role'>;
export const fromRoleEntityToFlatRole = ({
entity: roleEntity,
applicationIdToUniversalIdentifierMap,
}: FromRoleEntityToFlatRoleArgs): FlatRole => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(roleEntity.applicationId);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${roleEntity.applicationId} not found for role ${roleEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
export const fromRoleEntityToFlatRole = (role: RoleEntity): FlatRole => {
return {
id: role.id,
standardId: role.standardId,
label: role.label,
description: role.description,
icon: role.icon,
isEditable: role.isEditable,
canUpdateAllSettings: role.canUpdateAllSettings,
canAccessAllTools: role.canAccessAllTools,
canReadAllObjectRecords: role.canReadAllObjectRecords,
canUpdateAllObjectRecords: role.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords: role.canDestroyAllObjectRecords,
canBeAssignedToUsers: role.canBeAssignedToUsers,
canBeAssignedToAgents: role.canBeAssignedToAgents,
canBeAssignedToApiKeys: role.canBeAssignedToApiKeys,
workspaceId: role.workspaceId,
createdAt: role.createdAt.toISOString(),
updatedAt: role.updatedAt.toISOString(),
universalIdentifier: role.universalIdentifier,
applicationId: role.applicationId,
roleTargetIds: role.roleTargets.map((rt) => rt.id),
objectPermissionIds: role.objectPermissions.map((op) => op.id),
permissionFlagIds: role.permissionFlags.map((pf) => pf.id),
fieldPermissionIds: role.fieldPermissions.map((fp) => fp.id),
rowLevelPermissionPredicateIds: role.rowLevelPermissionPredicates.map(
(rp) => rp.id,
id: roleEntity.id,
standardId: roleEntity.standardId,
label: roleEntity.label,
description: roleEntity.description,
icon: roleEntity.icon,
isEditable: roleEntity.isEditable,
canUpdateAllSettings: roleEntity.canUpdateAllSettings,
canAccessAllTools: roleEntity.canAccessAllTools,
canReadAllObjectRecords: roleEntity.canReadAllObjectRecords,
canUpdateAllObjectRecords: roleEntity.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords: roleEntity.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords: roleEntity.canDestroyAllObjectRecords,
canBeAssignedToUsers: roleEntity.canBeAssignedToUsers,
canBeAssignedToAgents: roleEntity.canBeAssignedToAgents,
canBeAssignedToApiKeys: roleEntity.canBeAssignedToApiKeys,
workspaceId: roleEntity.workspaceId,
createdAt: roleEntity.createdAt.toISOString(),
updatedAt: roleEntity.updatedAt.toISOString(),
universalIdentifier: roleEntity.universalIdentifier,
applicationId: roleEntity.applicationId,
roleTargetIds: roleEntity.roleTargets.map(({ id }) => id),
objectPermissionIds: roleEntity.objectPermissions.map(({ id }) => id),
permissionFlagIds: roleEntity.permissionFlags.map(({ id }) => id),
fieldPermissionIds: roleEntity.fieldPermissions.map(({ id }) => id),
rowLevelPermissionPredicateIds: roleEntity.rowLevelPermissionPredicates.map(
({ id }) => id,
),
rowLevelPermissionPredicateGroupIds:
role.rowLevelPermissionPredicateGroups.map((rp) => rp.id),
roleEntity.rowLevelPermissionPredicateGroups.map(({ id }) => id),
__universal: {
universalIdentifier: roleEntity.universalIdentifier,
applicationUniversalIdentifier,
roleTargetUniversalIdentifiers: roleEntity.roleTargets.map(
({ universalIdentifier }) => universalIdentifier,
),
rowLevelPermissionPredicateUniversalIdentifiers:
roleEntity.rowLevelPermissionPredicates.map(
({ universalIdentifier }) => universalIdentifier,
),
rowLevelPermissionPredicateGroupUniversalIdentifiers:
roleEntity.rowLevelPermissionPredicateGroups.map(
({ universalIdentifier }) => universalIdentifier,
),
},
};
};
@@ -7,11 +7,17 @@ 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 { fromRowLevelPermissionPredicateGroupEntityToFlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/utils/from-row-level-permission-predicate-group-entity-to-flat-row-level-permission-predicate-group.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
import { type FlatRowLevelPermissionPredicateGroupMaps } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group-maps.type';
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 { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@@ -20,6 +26,14 @@ export class WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService extend
constructor(
@InjectRepository(RowLevelPermissionPredicateGroupEntity)
private readonly rowLevelPermissionPredicateGroupRepository: Repository<RowLevelPermissionPredicateGroupEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(RowLevelPermissionPredicateEntity)
private readonly rowLevelPermissionPredicateRepository: Repository<RowLevelPermissionPredicateEntity>,
) {
super();
}
@@ -27,13 +41,67 @@ export class WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService extend
async computeForCache(
workspaceId: string,
): Promise<FlatRowLevelPermissionPredicateGroupMaps> {
const rowLevelPermissionPredicateGroups =
await this.rowLevelPermissionPredicateGroupRepository.find({
where: {
workspaceId,
},
const [
rowLevelPermissionPredicateGroups,
applications,
objectMetadatas,
roles,
rowLevelPermissionPredicates,
] = await Promise.all([
this.rowLevelPermissionPredicateGroupRepository.find({
where: { workspaceId },
withDeleted: true,
});
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.objectMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.roleRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.rowLevelPermissionPredicateRepository.find({
where: { workspaceId },
select: [
'id',
'universalIdentifier',
'rowLevelPermissionPredicateGroupId',
],
withDeleted: true,
}),
]);
const [
childRowLevelPermissionPredicateGroupsByParentId,
rowLevelPermissionPredicatesByGroupId,
] = (
[
{
entities: rowLevelPermissionPredicateGroups,
foreignKey: 'parentRowLevelPermissionPredicateGroupId',
},
{
entities: rowLevelPermissionPredicates,
foreignKey: 'rowLevelPermissionPredicateGroupId',
},
] as const
).map(regroupEntitiesByRelatedEntityId);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);
const rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(rowLevelPermissionPredicateGroups);
const flatRowLevelPermissionPredicateGroupMaps =
createEmptyFlatEntityMaps();
@@ -41,7 +109,23 @@ export class WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService extend
for (const rowLevelPermissionPredicateGroupEntity of rowLevelPermissionPredicateGroups) {
const flatRowLevelPermissionPredicateGroup =
fromRowLevelPermissionPredicateGroupEntityToFlatRowLevelPermissionPredicateGroup(
rowLevelPermissionPredicateGroupEntity,
{
entity: {
...rowLevelPermissionPredicateGroupEntity,
childRowLevelPermissionPredicateGroups:
childRowLevelPermissionPredicateGroupsByParentId.get(
rowLevelPermissionPredicateGroupEntity.id,
) || [],
rowLevelPermissionPredicates:
rowLevelPermissionPredicatesByGroupId.get(
rowLevelPermissionPredicateGroupEntity.id,
) || [],
},
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap,
},
);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
@@ -7,11 +7,17 @@ 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 { 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 { fromRowLevelPermissionPredicateEntityToFlatRowLevelPermissionPredicate } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/utils/from-row-level-permission-predicate-entity-to-flat-row-level-permission-predicate.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
import { type FlatRowLevelPermissionPredicateMaps } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-maps.type';
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()
@@ -20,6 +26,16 @@ export class WorkspaceFlatRowLevelPermissionPredicateMapCacheService extends Wor
constructor(
@InjectRepository(RowLevelPermissionPredicateEntity)
private readonly rowLevelPermissionPredicateRepository: Repository<RowLevelPermissionPredicateEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(RowLevelPermissionPredicateGroupEntity)
private readonly rowLevelPermissionPredicateGroupRepository: Repository<RowLevelPermissionPredicateGroupEntity>,
) {
super();
}
@@ -27,21 +43,68 @@ export class WorkspaceFlatRowLevelPermissionPredicateMapCacheService extends Wor
async computeForCache(
workspaceId: string,
): Promise<FlatRowLevelPermissionPredicateMaps> {
const rowLevelPermissionPredicates =
await this.rowLevelPermissionPredicateRepository.find({
where: {
workspaceId,
},
const [
rowLevelPermissionPredicates,
applications,
fieldMetadatas,
objectMetadatas,
roles,
rowLevelPermissionPredicateGroups,
] = await Promise.all([
this.rowLevelPermissionPredicateRepository.find({
where: { workspaceId },
withDeleted: true,
});
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.objectMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.roleRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.rowLevelPermissionPredicateGroupRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const fieldMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(fieldMetadatas);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);
const rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(rowLevelPermissionPredicateGroups);
const flatRowLevelPermissionPredicateMaps = createEmptyFlatEntityMaps();
for (const rowLevelPermissionPredicateEntity of rowLevelPermissionPredicates) {
const flatRowLevelPermissionPredicate =
fromRowLevelPermissionPredicateEntityToFlatRowLevelPermissionPredicate(
rowLevelPermissionPredicateEntity,
);
fromRowLevelPermissionPredicateEntityToFlatRowLevelPermissionPredicate({
entity: rowLevelPermissionPredicateEntity,
applicationIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatRowLevelPermissionPredicate,
@@ -1,20 +1,117 @@
/* @license Enterprise */
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { ROW_LEVEL_PERMISSION_PREDICATE_ENTITY_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/constants/row-level-permission-predicate-entity-relation-properties.constant';
import { type RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
import { type FlatRowLevelPermissionPredicate } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromRowLevelPermissionPredicateEntityToFlatRowLevelPermissionPredicate =
(
rowLevelPermissionPredicateEntity: RowLevelPermissionPredicateEntity,
): FlatRowLevelPermissionPredicate => {
({
entity: rowLevelPermissionPredicateEntity,
applicationIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'rowLevelPermissionPredicate'>): FlatRowLevelPermissionPredicate => {
const rowLevelPermissionPredicateEntityWithoutRelations =
removePropertiesFromRecord(rowLevelPermissionPredicateEntity, [
...ROW_LEVEL_PERMISSION_PREDICATE_ENTITY_RELATION_PROPERTIES,
] as (typeof ROW_LEVEL_PERMISSION_PREDICATE_ENTITY_RELATION_PROPERTIES)[number][]);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
rowLevelPermissionPredicateEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${rowLevelPermissionPredicateEntity.applicationId} not found for rowLevelPermissionPredicate ${rowLevelPermissionPredicateEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const fieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(
rowLevelPermissionPredicateEntity.fieldMetadataId,
);
if (!isDefined(fieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`FieldMetadata with id ${rowLevelPermissionPredicateEntity.fieldMetadataId} not found for rowLevelPermissionPredicate ${rowLevelPermissionPredicateEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const objectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
rowLevelPermissionPredicateEntity.objectMetadataId,
);
if (!isDefined(objectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ObjectMetadata with id ${rowLevelPermissionPredicateEntity.objectMetadataId} not found for rowLevelPermissionPredicate ${rowLevelPermissionPredicateEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
rowLevelPermissionPredicateEntity.roleId,
);
if (!isDefined(roleUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Role with id ${rowLevelPermissionPredicateEntity.roleId} not found for rowLevelPermissionPredicate ${rowLevelPermissionPredicateEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let workspaceMemberFieldMetadataUniversalIdentifier: string | null = null;
if (
isDefined(
rowLevelPermissionPredicateEntity.workspaceMemberFieldMetadataId,
)
) {
workspaceMemberFieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(
rowLevelPermissionPredicateEntity.workspaceMemberFieldMetadataId,
) ?? null;
if (!isDefined(workspaceMemberFieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`FieldMetadata with id ${rowLevelPermissionPredicateEntity.workspaceMemberFieldMetadataId} not found for rowLevelPermissionPredicate ${rowLevelPermissionPredicateEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
let rowLevelPermissionPredicateGroupUniversalIdentifier: string | null =
null;
if (
isDefined(
rowLevelPermissionPredicateEntity.rowLevelPermissionPredicateGroupId,
)
) {
rowLevelPermissionPredicateGroupUniversalIdentifier =
rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap.get(
rowLevelPermissionPredicateEntity.rowLevelPermissionPredicateGroupId,
) ?? null;
if (!isDefined(rowLevelPermissionPredicateGroupUniversalIdentifier)) {
throw new FlatEntityMapsException(
`RowLevelPermissionPredicateGroup with id ${rowLevelPermissionPredicateEntity.rowLevelPermissionPredicateGroupId} not found for rowLevelPermissionPredicate ${rowLevelPermissionPredicateEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
return {
...rowLevelPermissionPredicateEntityWithoutRelations,
createdAt: rowLevelPermissionPredicateEntity.createdAt.toISOString(),
@@ -23,5 +120,16 @@ export const fromRowLevelPermissionPredicateEntityToFlatRowLevelPermissionPredic
rowLevelPermissionPredicateEntity.deletedAt?.toISOString() ?? null,
universalIdentifier:
rowLevelPermissionPredicateEntityWithoutRelations.universalIdentifier,
__universal: {
universalIdentifier:
rowLevelPermissionPredicateEntity.universalIdentifier,
applicationUniversalIdentifier,
fieldMetadataUniversalIdentifier,
objectMetadataUniversalIdentifier,
roleUniversalIdentifier,
workspaceMemberFieldMetadataUniversalIdentifier,
rowLevelPermissionPredicateGroupUniversalIdentifier,
value: rowLevelPermissionPredicateEntityWithoutRelations.value,
},
};
};
@@ -1,20 +1,95 @@
/* @license Enterprise */
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { ROW_LEVEL_PERMISSION_PREDICATE_GROUP_ENTITY_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/constants/row-level-permission-predicate-group-entity-relation-properties.constant';
import { type RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { type FlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
type FromRowLevelPermissionPredicateGroupEntityToFlatRowLevelPermissionPredicateGroupArgs =
FromEntityToFlatEntityArgs<'rowLevelPermissionPredicateGroup'> & {
rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap: Map<
string,
string
>;
};
export const fromRowLevelPermissionPredicateGroupEntityToFlatRowLevelPermissionPredicateGroup =
(
rowLevelPermissionPredicateGroupEntity: RowLevelPermissionPredicateGroupEntity,
): FlatRowLevelPermissionPredicateGroup => {
({
entity: rowLevelPermissionPredicateGroupEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap,
}: FromRowLevelPermissionPredicateGroupEntityToFlatRowLevelPermissionPredicateGroupArgs): FlatRowLevelPermissionPredicateGroup => {
const rowLevelPermissionPredicateGroupEntityWithoutRelations =
removePropertiesFromRecord(rowLevelPermissionPredicateGroupEntity, [
...ROW_LEVEL_PERMISSION_PREDICATE_GROUP_ENTITY_RELATION_PROPERTIES,
] as (typeof ROW_LEVEL_PERMISSION_PREDICATE_GROUP_ENTITY_RELATION_PROPERTIES)[number][]);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
rowLevelPermissionPredicateGroupEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${rowLevelPermissionPredicateGroupEntity.applicationId} not found for rowLevelPermissionPredicateGroup ${rowLevelPermissionPredicateGroupEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const objectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
rowLevelPermissionPredicateGroupEntity.objectMetadataId,
);
if (!isDefined(objectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ObjectMetadata with id ${rowLevelPermissionPredicateGroupEntity.objectMetadataId} not found for rowLevelPermissionPredicateGroup ${rowLevelPermissionPredicateGroupEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
rowLevelPermissionPredicateGroupEntity.roleId,
);
if (!isDefined(roleUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Role with id ${rowLevelPermissionPredicateGroupEntity.roleId} not found for rowLevelPermissionPredicateGroup ${rowLevelPermissionPredicateGroupEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let parentRowLevelPermissionPredicateGroupUniversalIdentifier:
| string
| null = null;
if (
isDefined(
rowLevelPermissionPredicateGroupEntity.parentRowLevelPermissionPredicateGroupId,
)
) {
parentRowLevelPermissionPredicateGroupUniversalIdentifier =
rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap.get(
rowLevelPermissionPredicateGroupEntity.parentRowLevelPermissionPredicateGroupId,
) ?? null;
if (
!isDefined(parentRowLevelPermissionPredicateGroupUniversalIdentifier)
) {
throw new FlatEntityMapsException(
`RowLevelPermissionPredicateGroup with id ${rowLevelPermissionPredicateGroupEntity.parentRowLevelPermissionPredicateGroupId} not found for rowLevelPermissionPredicateGroup ${rowLevelPermissionPredicateGroupEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
return {
...rowLevelPermissionPredicateGroupEntityWithoutRelations,
createdAt: rowLevelPermissionPredicateGroupEntity.createdAt.toISOString(),
@@ -31,5 +106,21 @@ export const fromRowLevelPermissionPredicateGroupEntityToFlatRowLevelPermissionP
rowLevelPermissionPredicateGroupEntity.rowLevelPermissionPredicates ??
[]
).map(({ id }) => id),
__universal: {
universalIdentifier:
rowLevelPermissionPredicateGroupEntity.universalIdentifier,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
roleUniversalIdentifier,
parentRowLevelPermissionPredicateGroupUniversalIdentifier,
childRowLevelPermissionPredicateGroupUniversalIdentifiers: (
rowLevelPermissionPredicateGroupEntity.childRowLevelPermissionPredicateGroups ??
[]
).map(({ universalIdentifier }) => universalIdentifier),
rowLevelPermissionPredicateUniversalIdentifiers: (
rowLevelPermissionPredicateGroupEntity.rowLevelPermissionPredicates ??
[]
).map(({ universalIdentifier }) => universalIdentifier),
},
};
};
@@ -1,13 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceFlatSkillMapCacheService } from 'src/engine/metadata-modules/flat-skill/services/workspace-flat-skill-map-cache.service';
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
@Module({
imports: [
TypeOrmModule.forFeature([SkillEntity]),
TypeOrmModule.forFeature([ApplicationEntity, SkillEntity]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatSkillMapCacheService],
@@ -5,11 +5,13 @@ 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 { type FlatSkillMaps } from 'src/engine/metadata-modules/flat-skill/types/flat-skill-maps.type';
import { fromSkillEntityToFlatSkill } from 'src/engine/metadata-modules/flat-skill/utils/from-skill-entity-to-flat-skill.util';
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
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()
@@ -18,20 +20,35 @@ export class WorkspaceFlatSkillMapCacheService extends WorkspaceCacheProvider<Fl
constructor(
@InjectRepository(SkillEntity)
private readonly skillRepository: Repository<SkillEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatSkillMaps> {
const skills = await this.skillRepository.find({
where: { workspaceId },
withDeleted: true,
});
const [skills, applications] = await Promise.all([
this.skillRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const flatSkillMaps = createEmptyFlatEntityMaps();
for (const skillEntity of skills) {
const flatSkill = fromSkillEntityToFlatSkill(skillEntity);
const flatSkill = fromSkillEntityToFlatSkill({
entity: skillEntity,
applicationIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatSkill,
@@ -1,9 +1,26 @@
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
import { type SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromSkillEntityToFlatSkill = ({
entity: skillEntity,
applicationIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'skill'>): FlatSkill => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(skillEntity.applicationId);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${skillEntity.applicationId} not found for skill ${skillEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
export const fromSkillEntityToFlatSkill = (
skillEntity: SkillEntity,
): FlatSkill => {
return {
createdAt: skillEntity.createdAt.toISOString(),
updatedAt: skillEntity.updatedAt.toISOString(),
@@ -19,5 +36,9 @@ export const fromSkillEntityToFlatSkill = (
isActive: skillEntity.isActive,
universalIdentifier: skillEntity.universalIdentifier,
applicationId: skillEntity.applicationId,
__universal: {
universalIdentifier: skillEntity.universalIdentifier,
applicationUniversalIdentifier,
},
};
};
@@ -5,11 +5,15 @@ 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 { 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 { 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 { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
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()
@@ -18,22 +22,57 @@ export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvide
constructor(
@InjectRepository(ViewFieldEntity)
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatViewFieldMaps> {
const existingViewFields = await this.viewFieldRepository.find({
where: {
workspaceId,
},
withDeleted: true,
});
const [viewFields, applications, fieldMetadatas, views] = await Promise.all(
[
this.viewFieldRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.viewRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
],
);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const fieldMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(fieldMetadatas);
const viewIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(views);
const flatViewFieldMaps = createEmptyFlatEntityMaps();
for (const viewFieldEntity of existingViewFields) {
const flatViewField = fromViewFieldEntityToFlatViewField(viewFieldEntity);
for (const viewFieldEntity of viewFields) {
const flatViewField = fromViewFieldEntityToFlatViewField({
entity: viewFieldEntity,
applicationIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
viewIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatViewField,
@@ -1,22 +1,68 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
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 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';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromViewFieldEntityToFlatViewField = (
viewFieldEntity: ViewFieldEntity,
): FlatViewField => {
export const fromViewFieldEntityToFlatViewField = ({
entity: viewFieldEntity,
applicationIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
viewIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'viewField'>): FlatViewField => {
const viewFieldEntityWithoutRelations = removePropertiesFromRecord(
viewFieldEntity,
getMetadataEntityRelationProperties('viewField'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(viewFieldEntity.applicationId);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${viewFieldEntity.applicationId} not found for viewField ${viewFieldEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const fieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(
viewFieldEntity.fieldMetadataId,
);
if (!isDefined(fieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`FieldMetadata with id ${viewFieldEntity.fieldMetadataId} not found for viewField ${viewFieldEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const viewUniversalIdentifier = viewIdToUniversalIdentifierMap.get(
viewFieldEntity.viewId,
);
if (!isDefined(viewUniversalIdentifier)) {
throw new FlatEntityMapsException(
`View with id ${viewFieldEntity.viewId} not found for viewField ${viewFieldEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...viewFieldEntityWithoutRelations,
createdAt: viewFieldEntity.createdAt.toISOString(),
updatedAt: viewFieldEntity.updatedAt.toISOString(),
deletedAt: viewFieldEntity.deletedAt?.toISOString() ?? null,
universalIdentifier: viewFieldEntityWithoutRelations.universalIdentifier,
__universal: {
universalIdentifier: viewFieldEntity.universalIdentifier,
applicationUniversalIdentifier,
fieldMetadataUniversalIdentifier,
viewUniversalIdentifier,
},
};
};
@@ -5,12 +5,15 @@ import { type 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 { type FlatViewFilterGroupMaps } from 'src/engine/metadata-modules/flat-view-filter-group/types/flat-view-filter-group-maps.type';
import { fromViewFilterGroupEntityToFlatViewFilterGroup } from 'src/engine/metadata-modules/flat-view-filter-group/utils/from-view-filter-group-entity-to-flat-view-filter-group.util';
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
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 { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@@ -20,24 +23,39 @@ export class WorkspaceFlatViewFilterGroupMapCacheService extends WorkspaceCacheP
constructor(
@InjectRepository(ViewFilterGroupEntity)
private readonly viewFilterGroupRepository: Repository<ViewFilterGroupEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(ViewFilterEntity)
private readonly viewFilterRepository: Repository<ViewFilterEntity>,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatViewFilterGroupMaps> {
const [viewFilterGroups, viewFilters] = await Promise.all([
this.viewFilterGroupRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.viewFilterRepository.find({
where: { workspaceId },
select: ['id', 'viewFilterGroupId'],
withDeleted: true,
}),
]);
const [viewFilterGroups, applications, viewFilters, views] =
await Promise.all([
this.viewFilterGroupRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.viewFilterRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'viewFilterGroupId'],
withDeleted: true,
}),
this.viewRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const [viewFiltersByViewFilterGroupId, childViewFilterGroupsByParentId] = (
[
@@ -52,17 +70,31 @@ export class WorkspaceFlatViewFilterGroupMapCacheService extends WorkspaceCacheP
] as const
).map(regroupEntitiesByRelatedEntityId);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const viewFilterGroupIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(viewFilterGroups);
const viewIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(views);
const flatViewFilterGroupMaps = createEmptyFlatEntityMaps();
for (const viewFilterGroupEntity of viewFilterGroups) {
const flatViewFilterGroup =
fromViewFilterGroupEntityToFlatViewFilterGroup({
...viewFilterGroupEntity,
viewFilters:
viewFiltersByViewFilterGroupId.get(viewFilterGroupEntity.id) || [],
childViewFilterGroups:
childViewFilterGroupsByParentId.get(viewFilterGroupEntity.id) || [],
} as ViewFilterGroupEntity);
entity: {
...viewFilterGroupEntity,
viewFilters:
viewFiltersByViewFilterGroupId.get(viewFilterGroupEntity.id) ||
[],
childViewFilterGroups:
childViewFilterGroupsByParentId.get(viewFilterGroupEntity.id) ||
[],
},
applicationIdToUniversalIdentifierMap,
viewFilterGroupIdToUniversalIdentifierMap,
viewIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatViewFilterGroup,
@@ -1,17 +1,68 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
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 FlatViewFilterGroup } from 'src/engine/metadata-modules/flat-view-filter-group/types/flat-view-filter-group.type';
import { type ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromViewFilterGroupEntityToFlatViewFilterGroup = (
viewFilterGroupEntity: ViewFilterGroupEntity,
): FlatViewFilterGroup => {
type FromViewFilterGroupEntityToFlatViewFilterGroupArgs =
FromEntityToFlatEntityArgs<'viewFilterGroup'> & {
viewFilterGroupIdToUniversalIdentifierMap: Map<string, string>;
};
export const fromViewFilterGroupEntityToFlatViewFilterGroup = ({
entity: viewFilterGroupEntity,
applicationIdToUniversalIdentifierMap,
viewFilterGroupIdToUniversalIdentifierMap,
viewIdToUniversalIdentifierMap,
}: FromViewFilterGroupEntityToFlatViewFilterGroupArgs): FlatViewFilterGroup => {
const viewFilterGroupEntityWithoutRelations = removePropertiesFromRecord(
viewFilterGroupEntity,
getMetadataEntityRelationProperties('viewFilterGroup'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
viewFilterGroupEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${viewFilterGroupEntity.applicationId} not found for viewFilterGroup ${viewFilterGroupEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let parentViewFilterGroupUniversalIdentifier: string | null = null;
if (isDefined(viewFilterGroupEntity.parentViewFilterGroupId)) {
parentViewFilterGroupUniversalIdentifier =
viewFilterGroupIdToUniversalIdentifierMap.get(
viewFilterGroupEntity.parentViewFilterGroupId,
) ?? null;
if (!isDefined(parentViewFilterGroupUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ViewFilterGroup with id ${viewFilterGroupEntity.parentViewFilterGroupId} not found for viewFilterGroup ${viewFilterGroupEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
const viewUniversalIdentifier = viewIdToUniversalIdentifierMap.get(
viewFilterGroupEntity.viewId,
);
if (!isDefined(viewUniversalIdentifier)) {
throw new FlatEntityMapsException(
`View with id ${viewFilterGroupEntity.viewId} not found for viewFilterGroup ${viewFilterGroupEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...viewFilterGroupEntityWithoutRelations,
createdAt: viewFilterGroupEntity.createdAt.toISOString(),
@@ -19,12 +70,22 @@ export const fromViewFilterGroupEntityToFlatViewFilterGroup = (
deletedAt: viewFilterGroupEntity.deletedAt?.toISOString() ?? null,
universalIdentifier:
viewFilterGroupEntityWithoutRelations.universalIdentifier,
viewFilterIds:
viewFilterGroupEntity.viewFilters?.map((viewFilter) => viewFilter.id) ??
[],
viewFilterIds: viewFilterGroupEntity.viewFilters?.map(({ id }) => id) ?? [],
childViewFilterGroupIds:
viewFilterGroupEntity.childViewFilterGroups?.map(
(childGroup) => childGroup.id,
) ?? [],
viewFilterGroupEntity.childViewFilterGroups?.map(({ id }) => id) ?? [],
__universal: {
universalIdentifier: viewFilterGroupEntity.universalIdentifier,
applicationUniversalIdentifier,
parentViewFilterGroupUniversalIdentifier,
viewUniversalIdentifier,
viewFilterUniversalIdentifiers:
viewFilterGroupEntity.viewFilters?.map(
({ universalIdentifier }) => universalIdentifier,
) ?? [],
childViewFilterGroupUniversalIdentifiers:
viewFilterGroupEntity.childViewFilterGroups?.map(
({ universalIdentifier }) => universalIdentifier,
) ?? [],
},
};
};
@@ -5,11 +5,16 @@ 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 { 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 { type FlatViewFilterMaps } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter-maps.type';
import { fromViewFilterEntityToFlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/utils/from-view-filter-entity-to-flat-view-filter.util';
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
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()
@@ -18,23 +23,66 @@ export class WorkspaceFlatViewFilterMapCacheService extends WorkspaceCacheProvid
constructor(
@InjectRepository(ViewFilterEntity)
private readonly viewFilterRepository: Repository<ViewFilterEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ViewFilterGroupEntity)
private readonly viewFilterGroupRepository: Repository<ViewFilterGroupEntity>,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatViewFilterMaps> {
const viewFilters = await this.viewFilterRepository.find({
where: {
workspaceId,
},
withDeleted: true,
});
const [viewFilters, applications, fieldMetadatas, viewFilterGroups, views] =
await Promise.all([
this.viewFilterRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.viewFilterGroupRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.viewRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const fieldMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(fieldMetadatas);
const viewFilterGroupIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(viewFilterGroups);
const viewIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(views);
const flatViewFilterMaps = createEmptyFlatEntityMaps();
for (const viewFilterEntity of viewFilters) {
const flatViewFilter =
fromViewFilterEntityToFlatViewFilter(viewFilterEntity);
const flatViewFilter = fromViewFilterEntityToFlatViewFilter({
entity: viewFilterEntity,
applicationIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
viewFilterGroupIdToUniversalIdentifierMap,
viewIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatViewFilter,
@@ -1,22 +1,87 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
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 FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { type ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromViewFilterEntityToFlatViewFilter = (
viewFilterEntity: ViewFilterEntity,
): FlatViewFilter => {
export const fromViewFilterEntityToFlatViewFilter = ({
entity: viewFilterEntity,
applicationIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
viewFilterGroupIdToUniversalIdentifierMap,
viewIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'viewFilter'>): FlatViewFilter => {
const viewFilterEntityWithoutRelations = removePropertiesFromRecord(
viewFilterEntity,
getMetadataEntityRelationProperties('viewFilter'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(viewFilterEntity.applicationId);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${viewFilterEntity.applicationId} not found for viewFilter ${viewFilterEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const fieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(
viewFilterEntity.fieldMetadataId,
);
if (!isDefined(fieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`FieldMetadata with id ${viewFilterEntity.fieldMetadataId} not found for viewFilter ${viewFilterEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let viewFilterGroupUniversalIdentifier: string | null = null;
if (isDefined(viewFilterEntity.viewFilterGroupId)) {
viewFilterGroupUniversalIdentifier =
viewFilterGroupIdToUniversalIdentifierMap.get(
viewFilterEntity.viewFilterGroupId,
) ?? null;
if (!isDefined(viewFilterGroupUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ViewFilterGroup with id ${viewFilterEntity.viewFilterGroupId} not found for viewFilter ${viewFilterEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
const viewUniversalIdentifier = viewIdToUniversalIdentifierMap.get(
viewFilterEntity.viewId,
);
if (!isDefined(viewUniversalIdentifier)) {
throw new FlatEntityMapsException(
`View with id ${viewFilterEntity.viewId} not found for viewFilter ${viewFilterEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...viewFilterEntityWithoutRelations,
createdAt: viewFilterEntity.createdAt.toISOString(),
updatedAt: viewFilterEntity.updatedAt.toISOString(),
deletedAt: viewFilterEntity.deletedAt?.toISOString() ?? null,
universalIdentifier: viewFilterEntityWithoutRelations.universalIdentifier,
__universal: {
universalIdentifier: viewFilterEntity.universalIdentifier,
applicationUniversalIdentifier,
fieldMetadataUniversalIdentifier,
viewFilterGroupUniversalIdentifier,
viewUniversalIdentifier,
value: viewFilterEntityWithoutRelations.value,
},
};
};
@@ -5,11 +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 { FlatViewGroupMaps } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group-maps.type';
import { fromViewGroupEntityToFlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/utils/from-view-group-entity-to-flat-view-group.util';
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
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()
@@ -18,22 +21,45 @@ export class WorkspaceFlatViewGroupMapCacheService extends WorkspaceCacheProvide
constructor(
@InjectRepository(ViewGroupEntity)
private readonly viewGroupRepository: Repository<ViewGroupEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatViewGroupMaps> {
const existingViewGroups = await this.viewGroupRepository.find({
where: {
workspaceId,
},
withDeleted: true,
});
const [viewGroups, applications, views] = await Promise.all([
this.viewGroupRepository.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,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const viewIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(views);
const flatViewGroupMaps = createEmptyFlatEntityMaps();
for (const viewGroupEntity of existingViewGroups) {
const flatViewGroup = fromViewGroupEntityToFlatViewGroup(viewGroupEntity);
for (const viewGroupEntity of viewGroups) {
const flatViewGroup = fromViewGroupEntityToFlatViewGroup({
entity: viewGroupEntity,
applicationIdToUniversalIdentifierMap,
viewIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatViewGroup,
@@ -1,22 +1,54 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
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 FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromViewGroupEntityToFlatViewGroup = (
viewGroupEntity: ViewGroupEntity,
): FlatViewGroup => {
export const fromViewGroupEntityToFlatViewGroup = ({
entity: viewGroupEntity,
applicationIdToUniversalIdentifierMap,
viewIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'viewGroup'>): FlatViewGroup => {
const viewGroupEntityWithoutRelations = removePropertiesFromRecord(
viewGroupEntity,
getMetadataEntityRelationProperties('viewGroup'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(viewGroupEntity.applicationId);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${viewGroupEntity.applicationId} not found for viewGroup ${viewGroupEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const viewUniversalIdentifier = viewIdToUniversalIdentifierMap.get(
viewGroupEntity.viewId,
);
if (!isDefined(viewUniversalIdentifier)) {
throw new FlatEntityMapsException(
`View with id ${viewGroupEntity.viewId} not found for viewGroup ${viewGroupEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...viewGroupEntityWithoutRelations,
createdAt: viewGroupEntity.createdAt.toISOString(),
updatedAt: viewGroupEntity.updatedAt.toISOString(),
deletedAt: viewGroupEntity.deletedAt?.toISOString() ?? null,
universalIdentifier: viewGroupEntityWithoutRelations.universalIdentifier,
__universal: {
universalIdentifier: viewGroupEntity.universalIdentifier,
applicationUniversalIdentifier,
viewUniversalIdentifier,
},
};
};
@@ -1,7 +1,10 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceFlatViewMapCacheService } from 'src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
@@ -11,6 +14,9 @@ import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entit
@Module({
imports: [
TypeOrmModule.forFeature([
ApplicationEntity,
FieldMetadataEntity,
ObjectMetadataEntity,
ViewEntity,
ViewFieldEntity,
ViewFilterEntity,
@@ -5,15 +5,19 @@ import { type 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 { 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 { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type';
import { fromViewEntityToFlatView } from 'src/engine/metadata-modules/flat-view/utils/from-view-entity-to-flat-view.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
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 { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
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 { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@@ -23,6 +27,12 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider<Fla
constructor(
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ViewFieldEntity)
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
@InjectRepository(ViewFilterEntity)
@@ -36,33 +46,56 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider<Fla
}
async computeForCache(workspaceId: string): Promise<FlatViewMaps> {
const [views, viewFields, viewFilters, viewGroups, viewFilterGroups] =
await Promise.all([
this.viewRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.viewFieldRepository.find({
where: { workspaceId },
select: ['id', 'viewId'],
withDeleted: true,
}),
this.viewFilterRepository.find({
where: { workspaceId },
select: ['id', 'viewId'],
withDeleted: true,
}),
this.viewGroupRepository.find({
where: { workspaceId },
select: ['id', 'viewId'],
withDeleted: true,
}),
this.viewFilterGroupRepository.find({
where: { workspaceId },
select: ['id', 'viewId'],
withDeleted: true,
}),
]);
const [
views,
applications,
objectMetadatas,
fieldMetadatas,
viewFields,
viewFilters,
viewGroups,
viewFilterGroups,
] = await Promise.all([
this.viewRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.objectMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.viewFieldRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'viewId'],
withDeleted: true,
}),
this.viewFilterRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'viewId'],
withDeleted: true,
}),
this.viewGroupRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'viewId'],
withDeleted: true,
}),
this.viewFilterGroupRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'viewId'],
withDeleted: true,
}),
]);
const [
viewFieldsByViewId,
@@ -90,16 +123,28 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider<Fla
] as const
).map(regroupEntitiesByRelatedEntityId);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);
const fieldMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(fieldMetadatas);
const flatViewMaps = createEmptyFlatEntityMaps();
for (const viewEntity of views) {
const flatView = fromViewEntityToFlatView({
...viewEntity,
viewFields: viewFieldsByViewId.get(viewEntity.id) || [],
viewFilters: viewFiltersByViewId.get(viewEntity.id) || [],
viewGroups: viewGroupsByViewId.get(viewEntity.id) || [],
viewFilterGroups: viewFilterGroupsByViewId.get(viewEntity.id) || [],
} as ViewEntity);
entity: {
...viewEntity,
viewFields: viewFieldsByViewId.get(viewEntity.id) || [],
viewFilters: viewFiltersByViewId.get(viewEntity.id) || [],
viewGroups: viewGroupsByViewId.get(viewEntity.id) || [],
viewFilterGroups: viewFilterGroupsByViewId.get(viewEntity.id) || [],
},
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatView,
@@ -1,27 +1,123 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
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 FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromViewEntityToFlatView = (viewEntity: ViewEntity): FlatView => {
export const fromViewEntityToFlatView = ({
entity: viewEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'view'>): FlatView => {
const viewEntityWithoutRelations = removePropertiesFromRecord(
viewEntity,
getMetadataEntityRelationProperties('view'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(viewEntity.applicationId);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${viewEntity.applicationId} not found for view ${viewEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const objectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(viewEntity.objectMetadataId);
if (!isDefined(objectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ObjectMetadata with id ${viewEntity.objectMetadataId} not found for view ${viewEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
let kanbanAggregateOperationFieldMetadataUniversalIdentifier: string | null =
null;
if (isDefined(viewEntity.kanbanAggregateOperationFieldMetadataId)) {
kanbanAggregateOperationFieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(
viewEntity.kanbanAggregateOperationFieldMetadataId,
) ?? null;
if (!isDefined(kanbanAggregateOperationFieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`FieldMetadata with id ${viewEntity.kanbanAggregateOperationFieldMetadataId} not found for view ${viewEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
let calendarFieldMetadataUniversalIdentifier: string | null = null;
if (isDefined(viewEntity.calendarFieldMetadataId)) {
calendarFieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(
viewEntity.calendarFieldMetadataId,
) ?? null;
if (!isDefined(calendarFieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`FieldMetadata with id ${viewEntity.calendarFieldMetadataId} not found for view ${viewEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
let mainGroupByFieldMetadataUniversalIdentifier: string | null = null;
if (isDefined(viewEntity.mainGroupByFieldMetadataId)) {
mainGroupByFieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(
viewEntity.mainGroupByFieldMetadataId,
) ?? null;
if (!isDefined(mainGroupByFieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`FieldMetadata with id ${viewEntity.mainGroupByFieldMetadataId} not found for view ${viewEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
return {
...viewEntityWithoutRelations,
createdAt: viewEntity.createdAt.toISOString(),
updatedAt: viewEntity.updatedAt.toISOString(),
deletedAt: viewEntity.deletedAt?.toISOString() ?? null,
universalIdentifier: viewEntityWithoutRelations.universalIdentifier,
viewFieldIds: viewEntity.viewFields.map((viewField) => viewField.id),
viewFilterIds: viewEntity.viewFilters.map((viewFilter) => viewFilter.id),
viewGroupIds: viewEntity.viewGroups.map((viewGroup) => viewGroup.id),
viewFilterGroupIds:
viewEntity.viewFilterGroups?.map(
(viewFilterGroup) => viewFilterGroup.id,
) ?? [],
viewFieldIds: viewEntity.viewFields.map(({ id }) => id),
viewFilterIds: viewEntity.viewFilters.map(({ id }) => id),
viewGroupIds: viewEntity.viewGroups.map(({ id }) => id),
viewFilterGroupIds: viewEntity.viewFilterGroups?.map(({ id }) => id) ?? [],
__universal: {
universalIdentifier: viewEntity.universalIdentifier,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
kanbanAggregateOperationFieldMetadataUniversalIdentifier,
calendarFieldMetadataUniversalIdentifier,
mainGroupByFieldMetadataUniversalIdentifier,
viewFieldUniversalIdentifiers: viewEntity.viewFields.map(
({ universalIdentifier }) => universalIdentifier,
),
viewFilterUniversalIdentifiers: viewEntity.viewFilters.map(
({ universalIdentifier }) => universalIdentifier,
),
viewGroupUniversalIdentifiers: viewEntity.viewGroups.map(
({ universalIdentifier }) => universalIdentifier,
),
viewFilterGroupUniversalIdentifiers:
viewEntity.viewFilterGroups?.map(
({ universalIdentifier }) => universalIdentifier,
) ?? [],
},
};
};
@@ -1,13 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceFlatWebhookMapCacheService } from 'src/engine/metadata-modules/flat-webhook/services/workspace-flat-webhook-map-cache.service';
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
@Module({
imports: [
TypeOrmModule.forFeature([WebhookEntity]),
TypeOrmModule.forFeature([ApplicationEntity, WebhookEntity]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatWebhookMapCacheService],
@@ -5,11 +5,13 @@ import { IsNull, 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 { type FlatWebhookMaps } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook-maps.type';
import { fromWebhookEntityToFlatWebhook } from 'src/engine/metadata-modules/flat-webhook/utils/from-webhook-entity-to-flat-webhook.util';
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
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()
@@ -18,19 +20,33 @@ export class WorkspaceFlatWebhookMapCacheService extends WorkspaceCacheProvider<
constructor(
@InjectRepository(WebhookEntity)
private readonly webhookRepository: Repository<WebhookEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatWebhookMaps> {
const webhooks = await this.webhookRepository.find({
where: { workspaceId, deletedAt: IsNull() },
});
const [webhooks, applications] = await Promise.all([
this.webhookRepository.find({
where: { workspaceId, deletedAt: IsNull() },
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const flatWebhookMaps = createEmptyFlatEntityMaps();
for (const webhookEntity of webhooks) {
const flatWebhook = fromWebhookEntityToFlatWebhook(webhookEntity);
const flatWebhook = fromWebhookEntityToFlatWebhook({
entity: webhookEntity,
applicationIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatWebhook,
@@ -1,9 +1,26 @@
import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type';
import { type WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromWebhookEntityToFlatWebhook = ({
entity: webhookEntity,
applicationIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'webhook'>): FlatWebhook => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(webhookEntity.applicationId);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${webhookEntity.applicationId} not found for webhook ${webhookEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
export const fromWebhookEntityToFlatWebhook = (
webhookEntity: WebhookEntity,
): FlatWebhook => {
return {
id: webhookEntity.id,
targetUrl: webhookEntity.targetUrl,
@@ -16,5 +33,9 @@ export const fromWebhookEntityToFlatWebhook = (
createdAt: webhookEntity.createdAt.toISOString(),
updatedAt: webhookEntity.updatedAt.toISOString(),
deletedAt: webhookEntity.deletedAt?.toISOString() ?? null,
__universal: {
universalIdentifier: webhookEntity.universalIdentifier,
applicationUniversalIdentifier,
},
};
};
@@ -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,
},
};
};
@@ -11,7 +11,6 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@@ -21,6 +20,7 @@ import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permis
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
@Entity('objectMetadata')
@Unique('IDX_OBJECT_METADATA_NAME_SINGULAR_WORKSPACE_ID_UNIQUE', [
@@ -100,6 +100,7 @@ export class ObjectMetadataEntity
shortcut: string | null;
// TODO: This should not be nullable - legacy field introduced when label identifier was nullable
// TODO: This should be a joinColumn and we should have a FK on this too
@Column({ nullable: true, type: 'uuid' })
labelIdentifierFieldMetadataId: string | null;
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
@@ -34,6 +35,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
@Module({
imports: [
TypeOrmModule.forFeature([
ApplicationEntity,
RoleEntity,
RoleTargetEntity,
ObjectPermissionEntity,
@@ -5,6 +5,7 @@ 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 { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
@@ -17,6 +18,7 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
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 { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@@ -28,6 +30,8 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
constructor(
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(RoleTargetEntity)
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
@InjectRepository(ObjectPermissionEntity)
@@ -49,6 +53,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
): Promise<FlatEntityMaps<FlatRole>> {
const [
roles,
applications,
roleTargets,
objectPermissions,
permissionFlags,
@@ -60,9 +65,14 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.roleTargetRepository.find({
where: { workspaceId },
select: ['id', 'roleId'],
select: ['id', 'universalIdentifier', 'roleId'],
withDeleted: true,
}),
this.objectPermissionRepository.find({
@@ -82,12 +92,12 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
}),
this.rowLevelPermissionPredicateRepository.find({
where: { workspaceId },
select: ['id', 'roleId'],
select: ['id', 'universalIdentifier', 'roleId'],
withDeleted: true,
}),
this.rowLevelPermissionPredicateGroupRepository.find({
where: { workspaceId },
select: ['id', 'roleId'],
select: ['id', 'universalIdentifier', 'roleId'],
withDeleted: true,
}),
]);
@@ -128,20 +138,26 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
] as const
).map(regroupEntitiesByRelatedEntityId);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const flatRoleMaps = createEmptyFlatEntityMaps();
for (const roleEntity of roles) {
const flatRole = fromRoleEntityToFlatRole({
...roleEntity,
roleTargets: roleTargetsByRoleId.get(roleEntity.id) || [],
objectPermissions: objectPermissionsByRoleId.get(roleEntity.id) || [],
permissionFlags: permissionFlagsByRoleId.get(roleEntity.id) || [],
fieldPermissions: fieldPermissionsByRoleId.get(roleEntity.id) || [],
rowLevelPermissionPredicates:
rowLevelPermissionPredicatesByRoleId.get(roleEntity.id) || [],
rowLevelPermissionPredicateGroups:
rowLevelPermissionPredicateGroupsByRoleId.get(roleEntity.id) || [],
} as RoleEntity);
entity: {
...roleEntity,
roleTargets: roleTargetsByRoleId.get(roleEntity.id) || [],
objectPermissions: objectPermissionsByRoleId.get(roleEntity.id) || [],
permissionFlags: permissionFlagsByRoleId.get(roleEntity.id) || [],
fieldPermissions: fieldPermissionsByRoleId.get(roleEntity.id) || [],
rowLevelPermissionPredicates:
rowLevelPermissionPredicatesByRoleId.get(roleEntity.id) || [],
rowLevelPermissionPredicateGroups:
rowLevelPermissionPredicateGroupsByRoleId.get(roleEntity.id) || [],
},
applicationIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatRole,
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
@@ -17,7 +18,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
@Module({
imports: [
TypeOrmModule.forFeature([WebhookEntity]),
TypeOrmModule.forFeature([ApplicationEntity, WebhookEntity]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
WorkspaceCacheStorageModule,
@@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
@@ -16,6 +17,7 @@ import { type CreateWebhookInput } from 'src/engine/metadata-modules/webhook/dto
import { type UpdateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/update-webhook.input';
import { type WebhookDTO } from 'src/engine/metadata-modules/webhook/dtos/webhook.dto';
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@@ -24,6 +26,8 @@ export class WebhookService {
constructor(
@InjectRepository(WebhookEntity)
private readonly webhookRepository: Repository<WebhookEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
@@ -40,26 +44,54 @@ export class WebhookService {
}
async findAll(workspaceId: string): Promise<WebhookDTO[]> {
const webhooks = await this.webhookRepository.find({
where: { workspaceId, deletedAt: IsNull() },
order: { createdAt: 'ASC' },
});
const [webhooks, applications] = await Promise.all([
this.webhookRepository.find({
where: { workspaceId, deletedAt: IsNull() },
order: { createdAt: 'ASC' },
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
return webhooks
.map(fromWebhookEntityToFlatWebhook)
.map((webhookEntity) =>
fromWebhookEntityToFlatWebhook({
entity: webhookEntity,
applicationIdToUniversalIdentifierMap,
}),
)
.map(fromFlatWebhookToWebhookDto);
}
async findById(id: string, workspaceId: string): Promise<WebhookDTO | null> {
const webhook = await this.webhookRepository.findOne({
where: { id, workspaceId, deletedAt: IsNull() },
});
const [webhook, applications] = await Promise.all([
this.webhookRepository.findOne({
where: { id, workspaceId, deletedAt: IsNull() },
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
}),
]);
if (!isDefined(webhook)) {
return null;
}
return fromFlatWebhookToWebhookDto(fromWebhookEntityToFlatWebhook(webhook));
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
return fromFlatWebhookToWebhookDto(
fromWebhookEntityToFlatWebhook({
entity: webhook,
applicationIdToUniversalIdentifierMap,
}),
);
}
async create(
@@ -0,0 +1,14 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataManyToOneRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-related-metadata-names.type';
export type EntityManyToOneIdByUniversalIdentifierMaps<
T extends AllMetadataName,
> = {
[P in MetadataManyToOneRelatedMetadataNames<T> as `${P}IdToUniversalIdentifierMap`]: Map<
string,
string
>;
} & {
applicationIdToUniversalIdentifierMap: Map<string, string>;
};
@@ -0,0 +1,15 @@
import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type';
import { type RegroupedEntity } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
export type EntityWithRegroupedOneToManyRelations<
TEntity extends SyncableEntity,
> = Omit<
TEntity,
ExtractEntityOneToManyEntityRelationProperties<TEntity, SyncableEntity>
> & {
[P in ExtractEntityOneToManyEntityRelationProperties<
TEntity,
SyncableEntity
>]: RegroupedEntity[];
};
@@ -0,0 +1,9 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type EntityManyToOneIdByUniversalIdentifierMaps } from 'src/engine/workspace-cache/types/entity-many-to-one-id-by-universal-identifier-maps.type';
import { type EntityWithRegroupedOneToManyRelations } from 'src/engine/workspace-cache/types/entity-with-regrouped-one-to-many-relations.type';
export type FromEntityToFlatEntityArgs<T extends AllMetadataName> = {
entity: EntityWithRegroupedOneToManyRelations<MetadataEntity<T>>;
} & EntityManyToOneIdByUniversalIdentifierMaps<T>;
@@ -0,0 +1,11 @@
export const createIdToUniversalIdentifierMap = (
entities: { id: string; universalIdentifier: string }[],
): Map<string, string> => {
const map = new Map<string, string>();
for (const entity of entities) {
map.set(entity.id, entity.universalIdentifier);
}
return map;
};
@@ -4,6 +4,8 @@ import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type';
export type RegroupedEntity = { id: string; universalIdentifier: string };
export type RegroupEntitiesByRelatedEntityIdArgs<T extends AllMetadataName> =
MetadataManyToOneJoinColumn<T> extends never
? never
@@ -15,7 +17,7 @@ export const regroupEntitiesByRelatedEntityId = <T extends AllMetadataName>({
entities,
foreignKey,
}: RegroupEntitiesByRelatedEntityIdArgs<T>) => {
const entitiesByRelatedEntityId = new Map<string, { id: string }[]>();
const entitiesByRelatedEntityId = new Map<string, RegroupedEntity[]>();
for (const entity of entities) {
const currentRelatedEntityId = entity[
@@ -30,9 +32,10 @@ export const regroupEntitiesByRelatedEntityId = <T extends AllMetadataName>({
entitiesByRelatedEntityId.set(currentRelatedEntityId, []);
}
entitiesByRelatedEntityId
.get(currentRelatedEntityId)!
.push({ id: entity.id });
entitiesByRelatedEntityId.get(currentRelatedEntityId)!.push({
id: entity.id,
universalIdentifier: entity.universalIdentifier,
});
}
return entitiesByRelatedEntityId;
@@ -0,0 +1,56 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type ExtractJsonbProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type';
export const ALL_JSONB_PROPERTIES_BY_METADATA_NAME = {
fieldMetadata: {
defaultValue: 'defaultValue',
standardOverrides: 'standardOverrides',
options: 'options',
settings: 'settings',
},
objectMetadata: {
standardOverrides: 'standardOverrides',
duplicateCriteria: 'duplicateCriteria',
},
view: {},
viewField: {},
viewGroup: {},
viewFilter: { value: 'value' },
viewFilterGroup: {},
index: {},
role: {},
roleTarget: {},
rowLevelPermissionPredicate: { value: 'value' },
rowLevelPermissionPredicateGroup: {},
logicFunction: {
cronTriggerSettings: 'cronTriggerSettings',
databaseEventTriggerSettings: 'databaseEventTriggerSettings',
httpRouteTriggerSettings: 'httpRouteTriggerSettings',
publishedVersions: 'publishedVersions',
toolInputSchema: 'toolInputSchema',
},
webhook: {},
agent: {
responseFormat: 'responseFormat',
modelConfiguration: 'modelConfiguration',
},
skill: {},
pageLayout: {},
pageLayoutTab: {},
pageLayoutWidget: {
gridPosition: 'gridPosition',
configuration: 'configuration',
},
commandMenuItem: {},
navigationMenuItem: {},
frontComponent: {},
} as const satisfies {
[P in AllMetadataName]: {
[K in ExtractJsonbProperties<MetadataEntity<P>>]: K;
};
};
export type AllJsonbPropertiesForMetadataName<T extends AllMetadataName> =
keyof (typeof ALL_JSONB_PROPERTIES_BY_METADATA_NAME)[T];
@@ -18,24 +18,24 @@ type BrandedObjectWithRelation = JsonbProperty<ObjectWithRelation>;
// eslint-disable-next-line unused-imports/no-unused-vars
type ObjectAssertions = [
// Object with SerializedRelation: Id suffix renamed to UniversalIdentifier
// Object with SerializedRelation: Id suffix renamed to UniversalIdentifier, value becomes nullable
Expect<
Equal<
FormatRecordSerializedRelationProperties<ObjectWithRelation>,
{
name: string;
targetFieldMetadataUniversalIdentifier: SerializedRelation;
targetFieldMetadataUniversalIdentifier: SerializedRelation | null;
}
>
>,
// Branded object with SerializedRelation: renames property, preserves brand key
// Branded object with SerializedRelation: renames property, preserves brand key, value becomes nullable
Expect<
Equal<
FormatRecordSerializedRelationProperties<BrandedObjectWithRelation>,
{
name: string;
targetFieldMetadataUniversalIdentifier: SerializedRelation;
targetFieldMetadataUniversalIdentifier: SerializedRelation | null;
__JsonbPropertyBrand__?: undefined;
}
>
@@ -56,6 +56,13 @@ type PrimitiveAssertions = [
Expect<Equal<FormatRecordSerializedRelationProperties<string>, string>>,
Expect<Equal<FormatRecordSerializedRelationProperties<number>, number>>,
Expect<Equal<FormatRecordSerializedRelationProperties<null>, null>>,
// SerializedRelation becomes nullable
Expect<
Equal<
FormatRecordSerializedRelationProperties<SerializedRelation>,
SerializedRelation | null
>
>,
];
// eslint-disable-next-line unused-imports/no-unused-vars
@@ -66,7 +73,7 @@ type ArrayAssertions = [
FormatRecordSerializedRelationProperties<ObjectWithRelation[]>,
{
name: string;
targetFieldMetadataUniversalIdentifier: SerializedRelation;
targetFieldMetadataUniversalIdentifier: SerializedRelation | null;
}[]
>
>,
@@ -88,7 +95,7 @@ type ArrayAssertions = [
FormatRecordSerializedRelationProperties<ObjectWithRelation[][]>,
{
name: string;
targetFieldMetadataUniversalIdentifier: SerializedRelation;
targetFieldMetadataUniversalIdentifier: SerializedRelation | null;
}[][]
>
>,
@@ -102,7 +109,7 @@ type UnionAssertions = [
FormatRecordSerializedRelationProperties<ObjectWithRelation | null>,
{
name: string;
targetFieldMetadataUniversalIdentifier: SerializedRelation;
targetFieldMetadataUniversalIdentifier: SerializedRelation | null;
} | null
>
>,
@@ -113,7 +120,7 @@ type UnionAssertions = [
FormatRecordSerializedRelationProperties<(ObjectWithRelation | null)[]>,
({
name: string;
targetFieldMetadataUniversalIdentifier: SerializedRelation;
targetFieldMetadataUniversalIdentifier: SerializedRelation | null;
} | null)[]
>
>,
@@ -128,14 +135,14 @@ type MultipleRelationsObject = {
// eslint-disable-next-line unused-imports/no-unused-vars
type MultipleRelationsAssertions = [
// Multiple SerializedRelation properties: all get renamed
// Multiple SerializedRelation properties: all get renamed and become nullable
Expect<
Equal<
FormatRecordSerializedRelationProperties<MultipleRelationsObject>,
{
name: string;
sourceFieldUniversalIdentifier: SerializedRelation;
targetFieldUniversalIdentifier: SerializedRelation;
sourceFieldUniversalIdentifier: SerializedRelation | null;
targetFieldUniversalIdentifier: SerializedRelation | null;
regularId: string;
}
>
@@ -190,7 +197,7 @@ type NestedObjectAssertions = [
{
name: string;
nested: {
fieldMetadataUniversalIdentifier: SerializedRelation;
fieldMetadataUniversalIdentifier: SerializedRelation | null;
};
}
>
@@ -204,7 +211,7 @@ type NestedObjectAssertions = [
name: string;
level1: {
level2: {
targetUniversalIdentifier: SerializedRelation;
targetUniversalIdentifier: SerializedRelation | null;
};
};
}
@@ -217,9 +224,9 @@ type NestedObjectAssertions = [
FormatRecordSerializedRelationProperties<MixedNestedObject>,
{
name: string;
directRelationUniversalIdentifier: SerializedRelation;
directRelationUniversalIdentifier: SerializedRelation | null;
nested: {
nestedRelationUniversalIdentifier: SerializedRelation;
nestedRelationUniversalIdentifier: SerializedRelation | null;
plainField: string;
};
}
@@ -233,7 +240,7 @@ type NestedObjectAssertions = [
{
name: string;
nested: {
relationUniversalIdentifier: SerializedRelation;
relationUniversalIdentifier: SerializedRelation | null;
} | null;
}
>
@@ -246,9 +253,85 @@ type NestedObjectAssertions = [
{
name: string;
items: {
itemRelationUniversalIdentifier: SerializedRelation;
itemRelationUniversalIdentifier: SerializedRelation | null;
}[];
}
>
>,
];
// JSON Schema-like structures with Record<string, X> properties
// These should NOT have their 'properties' key renamed
type JsonSchemaLikeObject = {
type: 'object';
properties: Record<string, { type: string; description?: string }>;
required?: string[];
};
type JsonSchemaUnion =
| { type: 'text' }
| {
type: 'json';
schema: JsonSchemaLikeObject;
};
type BrandedJsonSchemaUnion = JsonbProperty<JsonSchemaUnion>;
// eslint-disable-next-line unused-imports/no-unused-vars
type RecordPropertyAssertions = [
// Object with 'properties' key containing Record<string, X>:
// The 'properties' key should NOT be renamed to 'propertiesUniversalIdentifier'
Expect<
Equal<
FormatRecordSerializedRelationProperties<JsonSchemaLikeObject>,
{
type: 'object';
properties: Record<string, { type: string; description?: string }>;
required?: string[];
}
>
>,
// Union type with JSON schema: properties inside schema should stay as 'properties'
Expect<
Equal<
FormatRecordSerializedRelationProperties<JsonSchemaUnion>,
| { type: 'text' }
| {
type: 'json';
schema: {
type: 'object';
properties: Record<string, { type: string; description?: string }>;
required?: string[];
};
}
>
>,
// Branded JSON schema union: should preserve structure, only add brand
Expect<
Equal<
FormatRecordSerializedRelationProperties<BrandedJsonSchemaUnion>,
| { type: 'text'; __JsonbPropertyBrand__?: undefined }
| {
type: 'json';
schema: {
type: 'object';
properties: Record<string, { type: string; description?: string }>;
required?: string[];
};
__JsonbPropertyBrand__?: undefined;
}
>
>,
// Plain Record<string, X> should pass through unchanged
Expect<
Equal<
FormatRecordSerializedRelationProperties<
Record<string, { value: number }>
>,
Record<string, { value: number }>
>
>,
];
@@ -98,7 +98,10 @@ type NarrowedExpectedResult = {
relationType: RelationType;
onDelete?: RelationOnDeleteAction | undefined;
joinColumnName?: string | null | undefined;
junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined;
junctionTargetFieldUniversalIdentifier?:
| SerializedRelation
| null
| undefined;
__JsonbPropertyBrand__?: undefined;
};
@@ -111,7 +114,10 @@ type SettingsExpectedResult =
relationType: RelationType;
onDelete?: RelationOnDeleteAction | undefined;
joinColumnName?: string | null | undefined;
junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined;
junctionTargetFieldUniversalIdentifier?:
| SerializedRelation
| null
| undefined;
__JsonbPropertyBrand__?: undefined;
}
| {
@@ -1,17 +1,40 @@
import { type ExtractSerializedRelationProperties } from 'twenty-shared/types';
import {
type ExtractSerializedRelationProperties,
type IsSerializedRelation,
} from 'twenty-shared/types';
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type';
// Determines if a property should be transformed to a universal identifier
// A property is transformed only if:
// 1. It matches ExtractSerializedRelationProperties (has the serialized relation brand)
// 2. Its value does NOT have a string index signature (not a Record<string, X>)
type ShouldTransformToUniversalIdentifier<T, P extends keyof T> = [P] extends [
ExtractSerializedRelationProperties<T>,
]
? string extends keyof NonNullable<T[P]>
? false
: true
: false;
export type FormatRecordSerializedRelationProperties<T> = T extends unknown
? T extends (infer U)[]
? FormatRecordSerializedRelationProperties<U>[]
: T extends string
? T
? // By definition we assume that any SerializedRelation are not enforced at pg scope through an FK
// Which mean that SerializedRelation might resolve to non-existent entities ( null )
IsSerializedRelation<T> extends true
? T | null
: T
: T extends object
? {
[P in keyof T as P extends ExtractSerializedRelationProperties<T> &
string
? `${RemoveSuffix<P, 'Id'>}UniversalIdentifier`
[P in keyof T as ShouldTransformToUniversalIdentifier<
T,
P
> extends true
? P extends string
? `${RemoveSuffix<P, 'Id'>}UniversalIdentifier`
: P
: P]: FormatRecordSerializedRelationProperties<T[P]>;
}
: T
@@ -1,16 +1,38 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type AddSuffixToEntityManyToOneProperties } from 'src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-many-to-one-properties.type';
import { type AddSuffixToEntityOneToManyProperties } from 'src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type';
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type';
import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type';
import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modules/flat-entity/types/from-metadata-entity-to-metadata-name.type';
import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { type ExtractJsonbProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type';
import { type AllJsonbPropertiesForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-by-metadata-name.constant';
import { type FormatRecordSerializedRelationProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type';
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type';
// TODO Handle universal settings
export type UniversalSyncableFlatEntity = Omit<
SyncableEntity,
'application' | 'applicationId' | 'workspace' | 'workspaceId'
> & {
applicationUniversalIdentifier: string;
};
export type UniversalFlatEntityExtraProperties<
TEntity extends SyncableEntity,
TMetadataName extends
AllMetadataName = FromMetadataEntityToMetadataName<TEntity>,
> = AddSuffixToEntityOneToManyProperties<TEntity, 'universalIdentifiers'> &
AddSuffixToEntityManyToOneProperties<
TEntity,
TMetadataName,
'universalIdentifier'
> & {
applicationUniversalIdentifier: string;
} & {
[P in AllJsonbPropertiesForMetadataName<TMetadataName> &
keyof TEntity]: FormatRecordSerializedRelationProperties<TEntity[P]>;
};
export type UniversalFlatEntityFrom<
TEntity extends SyncableEntity,
TMetadataName extends
@@ -24,21 +46,7 @@ export type UniversalFlatEntityFrom<
| ExtractEntityRelatedEntityProperties<TEntity>
| Extract<MetadataManyToOneJoinColumn<TMetadataName>, keyof TEntity>
| keyof CastRecordTypeOrmDatePropertiesToString<TEntity>
| ExtractJsonbProperties<TEntity>
| AllJsonbPropertiesForMetadataName<TMetadataName>
> &
CastRecordTypeOrmDatePropertiesToString<TEntity> & {
[P in ExtractEntityOneToManyEntityRelationProperties<
TEntity,
SyncableEntity
> &
string as `${RemoveSuffix<P, 's'>}UniversalIdentifiers`]: string[];
} & {
[P in Extract<MetadataManyToOneJoinColumn<TMetadataName>, keyof TEntity> &
string as `${RemoveSuffix<P, 'Id'>}UniversalIdentifier`]: TEntity[P];
} & {
applicationUniversalIdentifier: string;
} & {
[P in ExtractJsonbProperties<TEntity>]: FormatRecordSerializedRelationProperties<
TEntity[P]
>;
};
CastRecordTypeOrmDatePropertiesToString<TEntity> &
UniversalFlatEntityExtraProperties<TEntity, TMetadataName>;
@@ -0,0 +1,4 @@
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
export type UniversalFlatRole = UniversalFlatEntityFrom<RoleEntity, 'role'>;
@@ -1,10 +1,11 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a created entity 1`] = `
{
"createdFlatEntityMaps": {
"byId": {
"field-id-1": {
"__universal": {},
"applicationId": "application-id-1",
"calendarViewIds": [],
"createdAt": "2024-01-01T00:00:00.000Z",
@@ -69,6 +70,7 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a dele
"deletedFlatEntityMaps": {
"byId": {
"field-id-1": {
"__universal": {},
"applicationId": "application-id-1",
"calendarViewIds": [],
"createdAt": "2024-01-01T00:00:00.000Z",
@@ -151,6 +153,7 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create
"createdFlatEntityMaps": {
"byId": {
"field-id-3": {
"__universal": {},
"applicationId": "application-id-1",
"calendarViewIds": [],
"createdAt": "2024-01-01T00:00:00.000Z",
@@ -197,6 +200,7 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create
"deletedFlatEntityMaps": {
"byId": {
"field-id-2": {
"__universal": {},
"applicationId": "application-id-1",
"calendarViewIds": [],
"createdAt": "2024-01-01T00:00:00.000Z",
@@ -18,5 +18,6 @@ export type UpdateFieldAction =
export type DeleteFieldAction =
BaseDeleteWorkspaceMigrationAction<'fieldMetadata'> & {
// Remove this
objectMetadataId: string;
};
@@ -1,7 +1,5 @@
import { InjectRepository } from '@nestjs/typeorm';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -11,8 +9,8 @@ import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
@@ -24,8 +22,7 @@ export class ConnectedAccountDeleteOnePreQueryHook
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
async execute(
@@ -55,17 +52,18 @@ export class ConnectedAccountDeleteOnePreQueryHook
authContext as WorkspaceAuthContext,
);
const objectMetadataEntity =
await this.objectMetadataRepository.findOneOrFail({
where: {
nameSingular: 'messageChannel',
const { flatObjectMetadataMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId: workspace.id,
flatMapsKeys: ['flatObjectMetadataMaps'],
},
relations: ['fields', 'indexMetadatas', 'views'],
});
);
const flatObjectMetadata =
fromObjectMetadataEntityToFlatObjectMetadata(objectMetadataEntity);
const flatObjectMetadata = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECT_IDS.messageChannel,
});
// TODO: handle cascade events for delete
this.workspaceEventEmitter.emitDatabaseBatchEvent({
@@ -2,11 +2,15 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ConnectedAccountDeleteOnePreQueryHook } from 'src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook';
@Module({
imports: [NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity])],
imports: [
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [ConnectedAccountDeleteOnePreQueryHook],
})
export class ConnectedAccountQueryHookModule {}
@@ -1,14 +1,11 @@
import { type SERIALIZED_RELATION_BRAND } from './SerializedRelation.type';
type HasSerializedRelationPropertyBrand<T> =
typeof SERIALIZED_RELATION_BRAND extends keyof T ? true : false;
import { IsSerializedRelation } from "@/types/IsSerializedRelation.type";
export type ExtractSerializedRelationProperties<T> = T extends unknown
? T extends object
? {
[P in keyof T]-?: [NonNullable<T[P]>] extends [never]
? never
: HasSerializedRelationPropertyBrand<NonNullable<T[P]>> extends true
: IsSerializedRelation<NonNullable<T[P]>> extends true
? P
: never;
}[keyof T]
@@ -0,0 +1,4 @@
import { SERIALIZED_RELATION_BRAND } from '@/types/SerializedRelation.type';
export type IsSerializedRelation<T> =
typeof SERIALIZED_RELATION_BRAND extends keyof T ? true : false;
@@ -66,4 +66,20 @@ type Assertions = [
'rel'
>
>,
// Types with string index signatures should return never
// (string index signatures shouldn't be mistaken for the brand)
Expect<
Equal<ExtractSerializedRelationProperties<Record<string, string>>, never>
>,
Expect<
Equal<
ExtractSerializedRelationProperties<
Record<string, { type: string; description?: string }>
>,
never
>
>,
];
@@ -107,6 +107,7 @@ export type { FromTo } from './FromToType';
export { HTTPMethod } from './HttpMethod';
export type { IsEmptyRecord } from './IsEmptyRecord.type';
export type { IsExactly } from './IsExactly';
export type { IsSerializedRelation } from './IsSerializedRelation.type';
export type { LogicFunctionEvent } from './LogicFunctionEvent';
export { MessageParticipantRole } from './MessageParticipantRole';
export type { ModifiedProperties } from './ModifiedProperties';