V2 centralize relation optimistic logic (#15552)

# Introduction
This PR aims to deprecate having to manually handle optimistic side
effect foreign key addition in the whole v2 experience.
This PR implements the strong basis + builder refactor of the optimistic
computation of a given flat entity maps with its related flat entity
maps ( runner needs a small refactor on actions type definition first )
Flat entity maps updates through mutations are now only scoped to the
generic entity builder ( very isolated )

## What's next
- Refactor actions v2 type definition to gain grain over `metadataName`
and action operation ( `create` `delete` `update` ).
from `{type: 'create_view_field'}` to `{metadataName: 'view_field',
type: 'create' }`
- Use new optimistic tool computation tools
- Only invalidate impacted flat maps cache

## New tools
Strictly dynamically typed new flat entity maps tools
- `addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow`
-
`deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow`

## Unit test
Adding basic unit testing coverage to introduced tools

## `FlatEntityValidationArgs`
From 
```ts
export type FlatEntityValidationArgs<T extends AllMetadataName> = {
  flatEntityToValidate: MetadataFlatEntity<T>;
  optimisticFlatEntityMaps: MetadataFlatEntityMaps<T>;
  mutableDependencyOptimisticFlatEntityMaps: MetadataValidationRelatedFlatEntityMaps<T>;
  workspaceId: string;
  remainingFlatEntityMapsToValidate: MetadataFlatEntityMaps<T>;
  buildOptions: WorkspaceMigrationBuilderOptions;
};

```

To
```ts
export type FlatEntityValidationArgs<T extends AllMetadataName> = {
  flatEntityToValidate: MetadataFlatEntity<T>;
  optimisticFlatEntityMapsAndRelatedFlatEntityMaps: MetadataFlatEntityAndRelatedFlatEntityMapsForValidation<T>;
  workspaceId: string;
  remainingFlatEntityMapsToValidate: MetadataFlatEntityMaps<T>;
  buildOptions: WorkspaceMigrationBuilderOptions;
};

```
This commit is contained in:
Paul Rastoin
2025-11-04 12:28:28 +01:00
committed by GitHub
parent 281070423f
commit 3514054235
39 changed files with 946 additions and 807 deletions
@@ -2,6 +2,7 @@ import { type AllMetadataName } from 'twenty-shared/metadata';
import { type ExtractPropertiesThatEndsWithId } from 'twenty-shared/types';
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';
type ExtractEntityRelations<TEntity extends MetadataEntity<AllMetadataName>> = {
[K in ExtractPropertiesThatEndsWithId<TEntity, 'id' | 'workspaceId'>]: K;
@@ -11,46 +12,93 @@ type MetadataRelatedMetadataNames<T extends AllMetadataName> =
keyof ExtractEntityRelations<MetadataEntity<T>>;
type MetadataNameAndRelations = {
[T in AllMetadataName]: MetadataRelatedMetadataNames<T> extends never
[TSourceMetadataName in AllMetadataName]: MetadataRelatedMetadataNames<TSourceMetadataName> extends never
? Record<string, never>
: {
[P in MetadataRelatedMetadataNames<T>]?: AllMetadataName;
[K in MetadataRelatedMetadataNames<TSourceMetadataName>]?: {
[TTargetMetadataName in AllMetadataName]?: {
metadataName: TTargetMetadataName;
flatEntityForeignKeyAggregator: keyof MetadataFlatEntity<TTargetMetadataName>;
};
}[AllMetadataName];
};
};
export const ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY = {
fieldMetadata: {
objectMetadataId: 'objectMetadata',
objectMetadataId: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: 'fieldMetadataIds',
},
},
objectMetadata: {},
view: {
kanbanAggregateOperationFieldMetadataId: 'fieldMetadata',
calendarFieldMetadataId: 'fieldMetadata',
objectMetadataId: 'objectMetadata',
kanbanAggregateOperationFieldMetadataId: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'kanbanAggregateOperationViewIds',
},
calendarFieldMetadataId: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'calendarViewIds',
},
objectMetadataId: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: 'viewIds',
},
},
viewField: {
viewId: 'view',
fieldMetadataId: 'fieldMetadata',
viewId: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFieldIds',
},
fieldMetadataId: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'viewFieldIds',
},
},
viewGroup: {
viewId: 'view',
fieldMetadataId: 'fieldMetadata',
viewId: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewGroupIds',
},
fieldMetadataId: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'viewGroupIds',
},
},
index: {
objectMetadataId: 'objectMetadata',
objectMetadataId: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: 'indexMetadataIds',
},
},
serverlessFunction: {},
cronTrigger: {
serverlessFunctionId: 'serverlessFunction',
serverlessFunctionId: {
metadataName: 'serverlessFunction',
flatEntityForeignKeyAggregator: 'cronTriggerIds',
},
},
databaseEventTrigger: {
serverlessFunctionId: 'serverlessFunction',
serverlessFunctionId: {
metadataName: 'serverlessFunction',
flatEntityForeignKeyAggregator: 'databaseEventTriggerIds',
},
},
routeTrigger: {
serverlessFunctionId: 'serverlessFunction',
serverlessFunctionId: {
metadataName: 'serverlessFunction',
flatEntityForeignKeyAggregator: 'routeTriggerIds',
},
},
viewFilter: {
viewId: 'view',
fieldMetadataId: 'fieldMetadata',
viewId: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFilterIds',
},
fieldMetadataId: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'viewFilterIds',
},
},
} as const satisfies MetadataNameAndRelations;
@@ -0,0 +1,15 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type MetadataRelatedFlatEntityMapsKeys } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-flat-entity-maps-keys.type';
import { type MetadataToFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/types/metadata-to-flat-entity-maps-key';
import { type MetadataValidationRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/types/metadata-validation-related-metadata-names.type';
export type MetadataFlatEntityAndRelatedFlatEntityMapsForValidation<
T extends AllMetadataName,
> = Pick<
AllFlatEntityMaps,
| MetadataRelatedFlatEntityMapsKeys<T>
| MetadataToFlatEntityMapsKey<T>
| MetadataToFlatEntityMapsKey<MetadataValidationRelatedMetadataNames<T>>
>;
@@ -6,5 +6,5 @@ import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/fl
export type MetadataManyToOneRelatedMetadataNames<T extends AllMetadataName> =
Extract<
(typeof ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY)[T][MetadataManyToOneJoinColumn<T>],
AllMetadataName
>;
{ metadataName: AllMetadataName }
>['metadataName'];
@@ -1,16 +1,9 @@
import { type IsEmptyRecord } from 'twenty-shared/types';
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant';
export type MetadataValidationRelatedMetadataNames<T extends AllMetadataName> =
IsEmptyRecord<
(typeof ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION)[T]
> extends true
? undefined
: NonNullable<
Extract<
keyof (typeof ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION)[T],
AllMetadataName
>
>;
Extract<
keyof (typeof ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION)[T],
AllMetadataName
>;
@@ -0,0 +1,94 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type MetadataFlatEntityAndRelatedFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type';
import { addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
describe('addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow', () => {
it('should add a view and update related objectMetadata with viewId', () => {
const objectMetadataId = 'object-1';
const viewId = 'view-1';
const applicationId = '20202020-f3ad-452e-b5b6-2d49d3ea88b1';
const workspaceId = '20202020-bc64-4148-8a79-b3144f743694';
const mockObjectMetadata = getFlatObjectMetadataMock({
id: objectMetadataId,
universalIdentifier: 'object-universal-1',
viewIds: [],
fieldMetadataIds: [],
workspaceId,
imageIdentifierFieldMetadataId: '20202020-9d65-415f-b0e1-216a2e257ea4',
labelIdentifierFieldMetadataId: '20202020-1a62-405c-87fa-4d4fd215851b',
applicationId,
});
const mockFieldMEtadata = getFlatFieldMetadataMock({
objectMetadataId,
id: '202020-71a3-4856-a3d0-d08cea0ecec6',
type: FieldMetadataType.DATE,
workspaceId,
applicationId,
universalIdentifier: 'field-universal-1',
viewFieldIds: [],
viewGroupIds: [],
viewFilterIds: [],
calendarViewIds: [],
});
const mockView: Pick<FlatView, 'id'> & Partial<FlatView> = {
id: viewId,
workspaceId,
universalIdentifier: 'view-universal-1',
objectMetadataId: objectMetadataId,
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
applicationId,
calendarFieldMetadataId: mockFieldMEtadata.id,
};
const flatEntityAndRelatedMapsToMutate: MetadataFlatEntityAndRelatedFlatEntityMaps<'view'> =
{
flatFieldMetadataMaps: addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: mockFieldMEtadata,
flatEntityMaps: createEmptyFlatEntityMaps(),
}),
flatObjectMetadataMaps: addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: mockObjectMetadata,
flatEntityMaps: createEmptyFlatEntityMaps(),
}),
flatViewMaps: createEmptyFlatEntityMaps(),
};
addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
metadataName: 'view',
flatEntity: mockView as FlatView,
flatEntityAndRelatedMapsToMutate,
});
expect(
flatEntityAndRelatedMapsToMutate.flatViewMaps.byId[mockView.id],
).toMatchObject(mockView);
expect(
flatEntityAndRelatedMapsToMutate.flatObjectMetadataMaps.byId[
objectMetadataId
],
).toMatchObject<Partial<FlatObjectMetadata>>({
viewIds: [mockView.id],
});
expect(
flatEntityAndRelatedMapsToMutate.flatFieldMetadataMaps.byId[
mockFieldMEtadata.id
],
).toMatchObject<Partial<FlatFieldMetadata>>({
calendarViewIds: [mockView.id],
});
});
});
@@ -0,0 +1,104 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type MetadataFlatEntityAndRelatedFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
describe('deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow', () => {
it('should delete a view and update related objectMetadata and fieldMetadata by removing viewId', () => {
const objectMetadataId = 'object-1';
const viewId = 'view-1';
const applicationId = '20202020-f3ad-452e-b5b6-2d49d3ea88b1';
const workspaceId = '20202020-bc64-4148-8a79-b3144f743694';
const mockObjectMetadata = getFlatObjectMetadataMock({
id: objectMetadataId,
universalIdentifier: 'object-universal-1',
viewIds: [viewId, 'something-else'],
fieldMetadataIds: [],
workspaceId,
imageIdentifierFieldMetadataId: '20202020-9d65-415f-b0e1-216a2e257ea4',
labelIdentifierFieldMetadataId: '20202020-1a62-405c-87fa-4d4fd215851b',
applicationId,
});
const mockFieldMetadata = getFlatFieldMetadataMock({
objectMetadataId,
id: '20202020-4087-423b-852a-91f91acf2df2',
type: FieldMetadataType.DATE,
universalIdentifier: 'field-universal-1',
viewFieldIds: [],
viewGroupIds: [],
viewFilterIds: [],
workspaceId,
calendarViewIds: [viewId],
applicationId,
});
const mockView: Partial<FlatView> = {
id: viewId,
universalIdentifier: 'view-universal-1',
objectMetadataId: objectMetadataId,
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
workspaceId,
calendarFieldMetadataId: mockFieldMetadata.id,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
icon: 'icon',
isCompact: false,
name: 'View Name',
position: 0,
applicationId,
};
const flatEntityAndRelatedMapsToMutate: MetadataFlatEntityAndRelatedFlatEntityMaps<'view'> =
{
flatFieldMetadataMaps: addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: mockFieldMetadata,
flatEntityMaps: createEmptyFlatEntityMaps(),
}),
flatObjectMetadataMaps: addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: mockObjectMetadata,
flatEntityMaps: createEmptyFlatEntityMaps(),
}),
flatViewMaps: addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: mockView as FlatView,
flatEntityMaps: createEmptyFlatEntityMaps(),
}),
};
deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
metadataName: 'view',
flatEntity: mockView as FlatView,
flatEntityAndRelatedMapsToMutate,
});
expect(
flatEntityAndRelatedMapsToMutate.flatViewMaps.byId[viewId],
).toBeUndefined();
expect(
flatEntityAndRelatedMapsToMutate.flatObjectMetadataMaps.byId[
objectMetadataId
],
).toMatchObject<Partial<FlatObjectMetadata>>({
viewIds: ['something-else'],
});
expect(
flatEntityAndRelatedMapsToMutate.flatFieldMetadataMaps.byId[
mockFieldMetadata.id
],
).toMatchObject<Partial<FlatFieldMetadata>>({
calendarViewIds: [],
});
});
});
@@ -1,90 +0,0 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { type MetadataFlatEntityAndRelatedFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
type AddFlatEntityToFlatEntityAndRelatedEntityMapsOrThrowArgs<
T extends AllMetadataName,
> = {
metadataName: T;
flatEntity: MetadataFlatEntity<T>;
flatEntityAndRelatedMaps: MetadataFlatEntityAndRelatedFlatEntityMaps<T>;
};
export const addFlatEntityToFlatEntityAndRelatedEntityMapsOrThrow = <
T extends AllMetadataName,
>({
metadataName,
flatEntity,
flatEntityAndRelatedMaps: initialFlatEntityAndRelatedMaps,
}: AddFlatEntityToFlatEntityAndRelatedEntityMapsOrThrowArgs<T>): MetadataFlatEntityAndRelatedFlatEntityMaps<T> => {
const flatEntityMapsKey: keyof MetadataFlatEntityAndRelatedFlatEntityMaps<T> =
getMetadataFlatEntityMapsKey(metadataName);
const updatedFlatEntityMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity,
flatEntityMaps: initialFlatEntityAndRelatedMaps[flatEntityMapsKey],
});
// TODO refactor
const manyToOneRelatedMetadataName = Object.entries(
ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY[metadataName],
);
return manyToOneRelatedMetadataName.reduce(
(flatEntityAndRelatedMaps, [relatedMetadataName, foreignKey]) => {
const relatedFlatEntityMapsKey = getMetadataFlatEntityMapsKey(
relatedMetadataName as AllMetadataName,
);
const relatedFLatEntityMetadataMaps =
flatEntityAndRelatedMaps[relatedFlatEntityMapsKey];
const relatedFlatEntity = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatEntity[
foreignKey as keyof MetadataFlatEntity<T>
] as string,
flatEntityMaps: relatedFLatEntityMetadataMaps,
});
const foreignKeyAggregatorProperty = `${metadataName}Ids`;
if (
!Object.prototype.hasOwnProperty.call(
relatedFlatEntity,
foreignKeyAggregatorProperty,
)
) {
throw new FlatEntityMapsException(
'Should never occur, invalid cached format',
FlatEntityMapsExceptionCode.ENTITY_MALFORMED,
);
}
const updatedRelatedFlatEntityMetadataMaps = {
...relatedFlatEntity,
[foreignKeyAggregatorProperty]: [
...(relatedFlatEntity[
foreignKeyAggregatorProperty as keyof MetadataFlatEntity<T>
] as string[]),
flatEntity.id,
],
};
return {
...flatEntityAndRelatedMaps,
[relatedFlatEntityMapsKey]: updatedRelatedFlatEntityMetadataMaps,
};
},
{
...initialFlatEntityAndRelatedMaps,
[flatEntityMapsKey]: updatedFlatEntityMaps,
},
);
};
@@ -0,0 +1,101 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { type MetadataRelatedFlatEntityMapsKeys } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-flat-entity-maps-keys.type';
import { type MetadataFlatEntityAndRelatedFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/replace-flat-entity-in-flat-entity-maps-through-mutation-or-throw.util';
type AddFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrowArgs<
T extends AllMetadataName,
> = {
metadataName: T;
flatEntity: MetadataFlatEntity<T>;
flatEntityAndRelatedMapsToMutate: MetadataFlatEntityAndRelatedFlatEntityMaps<T>;
};
export const addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow =
<T extends AllMetadataName>({
metadataName,
flatEntity,
flatEntityAndRelatedMapsToMutate,
}: AddFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrowArgs<T>) => {
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(metadataName);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity,
flatEntityMapsToMutate:
flatEntityAndRelatedMapsToMutate[flatEntityMapsKey],
});
const manyToOneRelatedMetadataName = Object.entries(
ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY[metadataName],
) as Array<
[
keyof MetadataFlatEntity<T>,
{
metadataName: AllMetadataName;
flatEntityForeignKeyAggregator: keyof MetadataFlatEntity<AllMetadataName>;
},
]
>;
for (const [
foreignKey,
{ metadataName: relatedMetadataName, flatEntityForeignKeyAggregator },
] of manyToOneRelatedMetadataName) {
const relatedFlatEntityMapsKey =
getMetadataFlatEntityMapsKey(relatedMetadataName);
const relatedFlatEntityMetadataMaps = flatEntityAndRelatedMapsToMutate[
relatedFlatEntityMapsKey as MetadataRelatedFlatEntityMapsKeys<T>
] as FlatEntityMaps<MetadataFlatEntity<typeof relatedMetadataName>>;
const flatEntityRelatedEntityForeignKeyValue = flatEntity[foreignKey] as
| string
| undefined;
if (!isDefined(flatEntityRelatedEntityForeignKeyValue)) {
continue;
}
const relatedFlatEntity = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatEntityRelatedEntityForeignKeyValue,
flatEntityMaps: relatedFlatEntityMetadataMaps,
});
if (
!Object.prototype.hasOwnProperty.call(
relatedFlatEntity,
flatEntityForeignKeyAggregator,
)
) {
throw new FlatEntityMapsException(
`Should never occur, invalid flat entity typing. flat ${metadataName} should contain ${flatEntityForeignKeyAggregator}`,
FlatEntityMapsExceptionCode.ENTITY_MALFORMED,
);
}
const updatedRelatedEntity = {
...relatedFlatEntity,
[flatEntityForeignKeyAggregator]: [
...(relatedFlatEntity[
flatEntityForeignKeyAggregator
] as unknown as string[]),
flatEntity.id,
],
};
replaceFlatEntityInFlatEntityMapsThroughMutationOrThrow({
flatEntity: updatedRelatedEntity,
flatEntityMapsToMutate: relatedFlatEntityMetadataMaps,
});
}
};
@@ -0,0 +1,107 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { type MetadataRelatedFlatEntityMapsKeys } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-flat-entity-maps-keys.type';
import { type MetadataFlatEntityAndRelatedFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { deleteFlatEntityFromFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/delete-flat-entity-from-flat-entity-maps-through-mutation-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/replace-flat-entity-in-flat-entity-maps-through-mutation-or-throw.util';
type DeleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrowArgs<
T extends AllMetadataName,
> = {
metadataName: T;
flatEntity: MetadataFlatEntity<T>;
flatEntityAndRelatedMapsToMutate: MetadataFlatEntityAndRelatedFlatEntityMaps<T>;
};
export const deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow =
<T extends AllMetadataName>({
metadataName,
flatEntity,
flatEntityAndRelatedMapsToMutate,
}: DeleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrowArgs<T>) => {
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(metadataName);
deleteFlatEntityFromFlatEntityMapsThroughMutationOrThrow<
MetadataFlatEntity<T>
>({
entityToDeleteId: flatEntity.id,
flatEntityMapsToMutate:
flatEntityAndRelatedMapsToMutate[flatEntityMapsKey],
});
const manyToOneRelatedMetadataName = Object.entries(
ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY[metadataName],
) as Array<
[
keyof MetadataFlatEntity<T>,
{
metadataName: AllMetadataName;
flatEntityForeignKeyAggregator: keyof MetadataFlatEntity<AllMetadataName>;
},
]
>;
for (const [
foreignKey,
{ metadataName: relatedMetadataName, flatEntityForeignKeyAggregator },
] of manyToOneRelatedMetadataName) {
const relatedFlatEntityMapsKey =
getMetadataFlatEntityMapsKey(relatedMetadataName);
const relatedFlatEntityMetadataMaps = flatEntityAndRelatedMapsToMutate[
relatedFlatEntityMapsKey as MetadataRelatedFlatEntityMapsKeys<T>
] as FlatEntityMaps<MetadataFlatEntity<typeof relatedMetadataName>>;
const flatEntityRelatedEntityForeignKeyValue = flatEntity[foreignKey] as
| string
| undefined;
if (!isDefined(flatEntityRelatedEntityForeignKeyValue)) {
continue;
}
const relatedFlatEntity = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatEntityRelatedEntityForeignKeyValue,
flatEntityMaps: relatedFlatEntityMetadataMaps,
});
if (!isDefined(relatedFlatEntity)) {
continue;
}
if (
!Object.prototype.hasOwnProperty.call(
relatedFlatEntity,
flatEntityForeignKeyAggregator,
)
) {
throw new FlatEntityMapsException(
`Should never occur, invalid flat entity typing. flat ${metadataName} should contain ${flatEntityForeignKeyAggregator}`,
FlatEntityMapsExceptionCode.ENTITY_MALFORMED,
);
}
const updatedRelatedEntity = {
...relatedFlatEntity,
[flatEntityForeignKeyAggregator]: (
relatedFlatEntity[
flatEntityForeignKeyAggregator
] as unknown as string[]
).filter((id) => id !== flatEntity.id),
};
replaceFlatEntityInFlatEntityMapsThroughMutationOrThrow({
flatEntity: updatedRelatedEntity,
flatEntityMapsToMutate: relatedFlatEntityMetadataMaps,
});
}
};
@@ -7,9 +7,11 @@ import { type FlatFieldMetadataTypeValidationArgs } from 'src/engine/metadata-mo
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
export const validateMorphOrRelationFlatFieldMetadata = async ({
mutableDependencyOptimisticFlatEntityMaps: { flatObjectMetadataMaps },
flatEntityToValidate: flatFieldMetadataToValidate,
optimisticFlatEntityMaps: optimisticFlatFieldMetadataMaps,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatFieldMetadataMaps,
flatObjectMetadataMaps,
},
remainingFlatEntityMapsToValidate,
}: FlatFieldMetadataTypeValidationArgs<MorphOrRelationFieldMetadataType>): Promise<
FlatFieldMetadataValidationError[]
@@ -50,7 +52,7 @@ export const validateMorphOrRelationFlatFieldMetadata = async ({
const targetRelationFlatFieldMetadata =
remainingFlatEntityMapsToValidate?.byId[relationTargetFieldMetadataId] ??
optimisticFlatFieldMetadataMaps.byId[relationTargetFieldMetadataId];
flatFieldMetadataMaps.byId[relationTargetFieldMetadataId];
if (
isDefined(targetRelationFlatObjectMetadata) &&
@@ -9,7 +9,10 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module';
import { ServerlessFunctionTriggerJob } from 'src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
@@ -23,7 +26,12 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
imports: [
FileUploadModule,
NestjsQueryTypeOrmModule.forFeature([ServerlessFunctionEntity]),
TypeOrmModule.forFeature([FeatureFlagEntity]),
TypeOrmModule.forFeature([
FeatureFlagEntity,
DatabaseEventTriggerEntity,
CronTriggerEntity,
RouteTriggerEntity,
]),
FileModule,
ThrottlerModule,
AuditModule,
@@ -1,21 +1,22 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.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 {
SERVERLESS_FUNCTION_ENTITY_RELATION_PROPERTIES,
ServerlessFunctionEntity,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { fromServerlessFunctionEntityToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.type';
import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator';
import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@@ -28,6 +29,12 @@ export class WorkspaceFlatServerlessFunctionMapCacheService extends WorkspaceFla
cacheStorageService: CacheStorageService,
@InjectRepository(ServerlessFunctionEntity)
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
@InjectRepository(DatabaseEventTriggerEntity)
private readonly databaseEventTriggerRepository: Repository<DatabaseEventTriggerEntity>,
@InjectRepository(CronTriggerEntity)
private readonly cronTriggerRepository: Repository<CronTriggerEntity>,
@InjectRepository(RouteTriggerEntity)
private readonly routeTriggerRepository: Repository<RouteTriggerEntity>,
) {
super(cacheStorageService);
}
@@ -37,24 +44,73 @@ export class WorkspaceFlatServerlessFunctionMapCacheService extends WorkspaceFla
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatServerlessFunction>> {
const serverlessFunctions = await this.serverlessFunctionRepository.find({
where: {
workspaceId,
},
withDeleted: true,
});
const [
serverlessFunctions,
routeTriggers,
cronTriggers,
databaseEventTriggers,
] = await Promise.all([
this.serverlessFunctionRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.cronTriggerRepository.find({
where: { workspaceId },
select: ['id', 'serverlessFunctionId'],
withDeleted: true,
}),
this.routeTriggerRepository.find({
where: { workspaceId },
select: ['id', 'serverlessFunctionId'],
withDeleted: true,
}),
this.databaseEventTriggerRepository.find({
where: { workspaceId },
select: ['id', 'serverlessFunctionId'],
withDeleted: true,
}),
]);
const [
routeTriggersByServerlessFunctionId,
cronTriggersByServerlessFunctionId,
databaseEventTriggersByServerlessFunctionId,
] = (
[
{
entities: routeTriggers,
foreignKey: 'serverlessFunctionId',
},
{
entities: cronTriggers,
foreignKey: 'serverlessFunctionId',
},
{
entities: databaseEventTriggers,
foreignKey: 'serverlessFunctionId',
},
] as const
).map(regroupEntitiesByRelatedEntityId);
const flatServerlessFunctionMaps = createEmptyFlatEntityMaps();
for (const serverlessFunctionEntity of serverlessFunctions) {
const flatServerlessFunction = {
...removePropertiesFromRecord(serverlessFunctionEntity, [
...SERVERLESS_FUNCTION_ENTITY_RELATION_PROPERTIES,
]),
universalIdentifier:
serverlessFunctionEntity.universalIdentifier ??
serverlessFunctionEntity.id,
} satisfies FlatServerlessFunction;
const flatServerlessFunction =
fromServerlessFunctionEntityToFlatServerlessFunction({
...serverlessFunctionEntity,
routeTriggers:
routeTriggersByServerlessFunctionId.get(
serverlessFunctionEntity.id,
) || [],
cronTriggers:
cronTriggersByServerlessFunctionId.get(
serverlessFunctionEntity.id,
) || [],
databaseEventTriggers:
databaseEventTriggersByServerlessFunctionId.get(
serverlessFunctionEntity.id,
) || [],
} as ServerlessFunctionEntity);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatServerlessFunction,
@@ -1,3 +1,4 @@
import { type Sources } from 'src/engine/core-modules/file-storage/types/source.type';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { type CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { type DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
@@ -6,7 +7,6 @@ import { type RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigg
import { type ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
import { type Sources } from 'src/engine/core-modules/file-storage/types/source.type';
export type ServerlessFunctionEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
@@ -22,5 +22,8 @@ export type FlatServerlessFunction = FlatEntityFrom<
ServerlessFunctionEntity,
ServerlessFunctionEntityRelationProperties
> & {
databaseEventTriggerIds: string[];
cronTriggerIds: string[];
routeTriggerIds: string[];
code?: Sources;
};
@@ -24,6 +24,9 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({
const currentDate = new Date();
return {
cronTriggerIds: [],
databaseEventTriggerIds: [],
routeTriggerIds: [],
id,
name: rawCreateServerlessFunctionInput.name,
description: rawCreateServerlessFunctionInput.description ?? null,
@@ -0,0 +1,32 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
export const fromServerlessFunctionEntityToFlatServerlessFunction = (
serverlessFunctionEntity: ServerlessFunctionEntity,
): FlatServerlessFunction => {
const serverlessFunctionWithoutRelations = removePropertiesFromRecord(
serverlessFunctionEntity,
[
'databaseEventTriggers',
'routeTriggers',
'cronTriggers',
'serverlessFunctionLayer',
'application',
],
);
return {
...serverlessFunctionWithoutRelations,
cronTriggerIds:
serverlessFunctionEntity.cronTriggers.map((el) => el.id) ?? [],
routeTriggerIds:
serverlessFunctionEntity.routeTriggers.map((el) => el.id) ?? [],
databaseEventTriggerIds:
serverlessFunctionEntity.databaseEventTriggers.map((el) => el.id) ?? [],
universalIdentifier:
serverlessFunctionEntity.universalIdentifier ??
serverlessFunctionEntity.id,
};
};