Skip to content

Commit 3514054

Browse files
authored
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; }; ```
1 parent 2810704 commit 3514054

File tree

39 files changed

+946
-807
lines changed

39 files changed

+946
-807
lines changed

packages/twenty-server/src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
21
import { type ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
2+
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
33

44
export type ServerlessExecuteError = {
55
errorType: string;
@@ -15,6 +15,7 @@ export type ServerlessExecuteResult = {
1515
error?: ServerlessExecuteError;
1616
};
1717

18+
// TODO refactor to be using FlatServerlessFunction
1819
export interface ServerlessDriver {
1920
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
2021
execute(

packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant.ts

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type AllMetadataName } from 'twenty-shared/metadata';
22
import { type ExtractPropertiesThatEndsWithId } from 'twenty-shared/types';
33

44
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
5+
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
56

67
type ExtractEntityRelations<TEntity extends MetadataEntity<AllMetadataName>> = {
78
[K in ExtractPropertiesThatEndsWithId<TEntity, 'id' | 'workspaceId'>]: K;
@@ -11,46 +12,93 @@ type MetadataRelatedMetadataNames<T extends AllMetadataName> =
1112
keyof ExtractEntityRelations<MetadataEntity<T>>;
1213

1314
type MetadataNameAndRelations = {
14-
[T in AllMetadataName]: MetadataRelatedMetadataNames<T> extends never
15+
[TSourceMetadataName in AllMetadataName]: MetadataRelatedMetadataNames<TSourceMetadataName> extends never
1516
? Record<string, never>
1617
: {
17-
[P in MetadataRelatedMetadataNames<T>]?: AllMetadataName;
18+
[K in MetadataRelatedMetadataNames<TSourceMetadataName>]?: {
19+
[TTargetMetadataName in AllMetadataName]?: {
20+
metadataName: TTargetMetadataName;
21+
flatEntityForeignKeyAggregator: keyof MetadataFlatEntity<TTargetMetadataName>;
22+
};
23+
}[AllMetadataName];
1824
};
1925
};
2026

2127
export const ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY = {
2228
fieldMetadata: {
23-
objectMetadataId: 'objectMetadata',
29+
objectMetadataId: {
30+
metadataName: 'objectMetadata',
31+
flatEntityForeignKeyAggregator: 'fieldMetadataIds',
32+
},
2433
},
2534
objectMetadata: {},
2635
view: {
27-
kanbanAggregateOperationFieldMetadataId: 'fieldMetadata',
28-
calendarFieldMetadataId: 'fieldMetadata',
29-
objectMetadataId: 'objectMetadata',
36+
kanbanAggregateOperationFieldMetadataId: {
37+
metadataName: 'fieldMetadata',
38+
flatEntityForeignKeyAggregator: 'kanbanAggregateOperationViewIds',
39+
},
40+
calendarFieldMetadataId: {
41+
metadataName: 'fieldMetadata',
42+
flatEntityForeignKeyAggregator: 'calendarViewIds',
43+
},
44+
objectMetadataId: {
45+
metadataName: 'objectMetadata',
46+
flatEntityForeignKeyAggregator: 'viewIds',
47+
},
3048
},
3149
viewField: {
32-
viewId: 'view',
33-
fieldMetadataId: 'fieldMetadata',
50+
viewId: {
51+
metadataName: 'view',
52+
flatEntityForeignKeyAggregator: 'viewFieldIds',
53+
},
54+
fieldMetadataId: {
55+
metadataName: 'fieldMetadata',
56+
flatEntityForeignKeyAggregator: 'viewFieldIds',
57+
},
3458
},
3559
viewGroup: {
36-
viewId: 'view',
37-
fieldMetadataId: 'fieldMetadata',
60+
viewId: {
61+
metadataName: 'view',
62+
flatEntityForeignKeyAggregator: 'viewGroupIds',
63+
},
64+
fieldMetadataId: {
65+
metadataName: 'fieldMetadata',
66+
flatEntityForeignKeyAggregator: 'viewGroupIds',
67+
},
3868
},
3969
index: {
40-
objectMetadataId: 'objectMetadata',
70+
objectMetadataId: {
71+
metadataName: 'objectMetadata',
72+
flatEntityForeignKeyAggregator: 'indexMetadataIds',
73+
},
4174
},
4275
serverlessFunction: {},
4376
cronTrigger: {
44-
serverlessFunctionId: 'serverlessFunction',
77+
serverlessFunctionId: {
78+
metadataName: 'serverlessFunction',
79+
flatEntityForeignKeyAggregator: 'cronTriggerIds',
80+
},
4581
},
4682
databaseEventTrigger: {
47-
serverlessFunctionId: 'serverlessFunction',
83+
serverlessFunctionId: {
84+
metadataName: 'serverlessFunction',
85+
flatEntityForeignKeyAggregator: 'databaseEventTriggerIds',
86+
},
4887
},
4988
routeTrigger: {
50-
serverlessFunctionId: 'serverlessFunction',
89+
serverlessFunctionId: {
90+
metadataName: 'serverlessFunction',
91+
flatEntityForeignKeyAggregator: 'routeTriggerIds',
92+
},
5193
},
5294
viewFilter: {
53-
viewId: 'view',
54-
fieldMetadataId: 'fieldMetadata',
95+
viewId: {
96+
metadataName: 'view',
97+
flatEntityForeignKeyAggregator: 'viewFilterIds',
98+
},
99+
fieldMetadataId: {
100+
metadataName: 'fieldMetadata',
101+
flatEntityForeignKeyAggregator: 'viewFilterIds',
102+
},
55103
},
56104
} as const satisfies MetadataNameAndRelations;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { type AllMetadataName } from 'twenty-shared/metadata';
2+
3+
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
4+
import { type MetadataRelatedFlatEntityMapsKeys } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-flat-entity-maps-keys.type';
5+
import { type MetadataToFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/types/metadata-to-flat-entity-maps-key';
6+
import { type MetadataValidationRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/types/metadata-validation-related-metadata-names.type';
7+
8+
export type MetadataFlatEntityAndRelatedFlatEntityMapsForValidation<
9+
T extends AllMetadataName,
10+
> = Pick<
11+
AllFlatEntityMaps,
12+
| MetadataRelatedFlatEntityMapsKeys<T>
13+
| MetadataToFlatEntityMapsKey<T>
14+
| MetadataToFlatEntityMapsKey<MetadataValidationRelatedMetadataNames<T>>
15+
>;

packages/twenty-server/src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-related-metadata-names.type.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/fl
66
export type MetadataManyToOneRelatedMetadataNames<T extends AllMetadataName> =
77
Extract<
88
(typeof ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY)[T][MetadataManyToOneJoinColumn<T>],
9-
AllMetadataName
10-
>;
9+
{ metadataName: AllMetadataName }
10+
>['metadataName'];
Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
1-
import { type IsEmptyRecord } from 'twenty-shared/types';
21
import { type AllMetadataName } from 'twenty-shared/metadata';
32

43
import { type ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant';
54

65
export type MetadataValidationRelatedMetadataNames<T extends AllMetadataName> =
7-
IsEmptyRecord<
8-
(typeof ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION)[T]
9-
> extends true
10-
? undefined
11-
: NonNullable<
12-
Extract<
13-
keyof (typeof ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION)[T],
14-
AllMetadataName
15-
>
16-
>;
6+
Extract<
7+
keyof (typeof ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION)[T],
8+
AllMetadataName
9+
>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { FieldMetadataType } from 'twenty-shared/types';
2+
3+
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
4+
import { type MetadataFlatEntityAndRelatedFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type';
5+
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';
6+
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
7+
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
8+
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
9+
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
10+
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
11+
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
12+
13+
describe('addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow', () => {
14+
it('should add a view and update related objectMetadata with viewId', () => {
15+
const objectMetadataId = 'object-1';
16+
const viewId = 'view-1';
17+
const applicationId = '20202020-f3ad-452e-b5b6-2d49d3ea88b1';
18+
const workspaceId = '20202020-bc64-4148-8a79-b3144f743694';
19+
const mockObjectMetadata = getFlatObjectMetadataMock({
20+
id: objectMetadataId,
21+
universalIdentifier: 'object-universal-1',
22+
viewIds: [],
23+
fieldMetadataIds: [],
24+
workspaceId,
25+
imageIdentifierFieldMetadataId: '20202020-9d65-415f-b0e1-216a2e257ea4',
26+
labelIdentifierFieldMetadataId: '20202020-1a62-405c-87fa-4d4fd215851b',
27+
applicationId,
28+
});
29+
30+
const mockFieldMEtadata = getFlatFieldMetadataMock({
31+
objectMetadataId,
32+
id: '202020-71a3-4856-a3d0-d08cea0ecec6',
33+
type: FieldMetadataType.DATE,
34+
workspaceId,
35+
applicationId,
36+
universalIdentifier: 'field-universal-1',
37+
viewFieldIds: [],
38+
viewGroupIds: [],
39+
viewFilterIds: [],
40+
calendarViewIds: [],
41+
});
42+
43+
const mockView: Pick<FlatView, 'id'> & Partial<FlatView> = {
44+
id: viewId,
45+
workspaceId,
46+
universalIdentifier: 'view-universal-1',
47+
objectMetadataId: objectMetadataId,
48+
viewFieldIds: [],
49+
viewFilterIds: [],
50+
viewGroupIds: [],
51+
applicationId,
52+
calendarFieldMetadataId: mockFieldMEtadata.id,
53+
};
54+
55+
const flatEntityAndRelatedMapsToMutate: MetadataFlatEntityAndRelatedFlatEntityMaps<'view'> =
56+
{
57+
flatFieldMetadataMaps: addFlatEntityToFlatEntityMapsOrThrow({
58+
flatEntity: mockFieldMEtadata,
59+
flatEntityMaps: createEmptyFlatEntityMaps(),
60+
}),
61+
flatObjectMetadataMaps: addFlatEntityToFlatEntityMapsOrThrow({
62+
flatEntity: mockObjectMetadata,
63+
flatEntityMaps: createEmptyFlatEntityMaps(),
64+
}),
65+
flatViewMaps: createEmptyFlatEntityMaps(),
66+
};
67+
68+
addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
69+
metadataName: 'view',
70+
flatEntity: mockView as FlatView,
71+
flatEntityAndRelatedMapsToMutate,
72+
});
73+
74+
expect(
75+
flatEntityAndRelatedMapsToMutate.flatViewMaps.byId[mockView.id],
76+
).toMatchObject(mockView);
77+
78+
expect(
79+
flatEntityAndRelatedMapsToMutate.flatObjectMetadataMaps.byId[
80+
objectMetadataId
81+
],
82+
).toMatchObject<Partial<FlatObjectMetadata>>({
83+
viewIds: [mockView.id],
84+
});
85+
86+
expect(
87+
flatEntityAndRelatedMapsToMutate.flatFieldMetadataMaps.byId[
88+
mockFieldMEtadata.id
89+
],
90+
).toMatchObject<Partial<FlatFieldMetadata>>({
91+
calendarViewIds: [mockView.id],
92+
});
93+
});
94+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { FieldMetadataType } from 'twenty-shared/types';
2+
3+
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
4+
import { type MetadataFlatEntityAndRelatedFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type';
5+
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
6+
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';
7+
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
8+
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
9+
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
10+
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
11+
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
12+
13+
describe('deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow', () => {
14+
it('should delete a view and update related objectMetadata and fieldMetadata by removing viewId', () => {
15+
const objectMetadataId = 'object-1';
16+
const viewId = 'view-1';
17+
const applicationId = '20202020-f3ad-452e-b5b6-2d49d3ea88b1';
18+
const workspaceId = '20202020-bc64-4148-8a79-b3144f743694';
19+
20+
const mockObjectMetadata = getFlatObjectMetadataMock({
21+
id: objectMetadataId,
22+
universalIdentifier: 'object-universal-1',
23+
viewIds: [viewId, 'something-else'],
24+
fieldMetadataIds: [],
25+
workspaceId,
26+
imageIdentifierFieldMetadataId: '20202020-9d65-415f-b0e1-216a2e257ea4',
27+
labelIdentifierFieldMetadataId: '20202020-1a62-405c-87fa-4d4fd215851b',
28+
applicationId,
29+
});
30+
31+
const mockFieldMetadata = getFlatFieldMetadataMock({
32+
objectMetadataId,
33+
id: '20202020-4087-423b-852a-91f91acf2df2',
34+
type: FieldMetadataType.DATE,
35+
universalIdentifier: 'field-universal-1',
36+
viewFieldIds: [],
37+
viewGroupIds: [],
38+
viewFilterIds: [],
39+
workspaceId,
40+
calendarViewIds: [viewId],
41+
applicationId,
42+
});
43+
44+
const mockView: Partial<FlatView> = {
45+
id: viewId,
46+
universalIdentifier: 'view-universal-1',
47+
objectMetadataId: objectMetadataId,
48+
viewFieldIds: [],
49+
viewFilterIds: [],
50+
viewGroupIds: [],
51+
workspaceId,
52+
calendarFieldMetadataId: mockFieldMetadata.id,
53+
createdAt: new Date('2024-01-01'),
54+
updatedAt: new Date('2024-01-01'),
55+
icon: 'icon',
56+
isCompact: false,
57+
name: 'View Name',
58+
position: 0,
59+
applicationId,
60+
};
61+
62+
const flatEntityAndRelatedMapsToMutate: MetadataFlatEntityAndRelatedFlatEntityMaps<'view'> =
63+
{
64+
flatFieldMetadataMaps: addFlatEntityToFlatEntityMapsOrThrow({
65+
flatEntity: mockFieldMetadata,
66+
flatEntityMaps: createEmptyFlatEntityMaps(),
67+
}),
68+
flatObjectMetadataMaps: addFlatEntityToFlatEntityMapsOrThrow({
69+
flatEntity: mockObjectMetadata,
70+
flatEntityMaps: createEmptyFlatEntityMaps(),
71+
}),
72+
flatViewMaps: addFlatEntityToFlatEntityMapsOrThrow({
73+
flatEntity: mockView as FlatView,
74+
flatEntityMaps: createEmptyFlatEntityMaps(),
75+
}),
76+
};
77+
78+
deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
79+
metadataName: 'view',
80+
flatEntity: mockView as FlatView,
81+
flatEntityAndRelatedMapsToMutate,
82+
});
83+
84+
expect(
85+
flatEntityAndRelatedMapsToMutate.flatViewMaps.byId[viewId],
86+
).toBeUndefined();
87+
88+
expect(
89+
flatEntityAndRelatedMapsToMutate.flatObjectMetadataMaps.byId[
90+
objectMetadataId
91+
],
92+
).toMatchObject<Partial<FlatObjectMetadata>>({
93+
viewIds: ['something-else'],
94+
});
95+
96+
expect(
97+
flatEntityAndRelatedMapsToMutate.flatFieldMetadataMaps.byId[
98+
mockFieldMetadata.id
99+
],
100+
).toMatchObject<Partial<FlatFieldMetadata>>({
101+
calendarViewIds: [],
102+
});
103+
});
104+
});

0 commit comments

Comments
 (0)