[OBJECT_MANIFEST_BREAKING_CHANGE] Sync returns workspace migration (#17918)

# Introduction
In this PR we start returning a workspace migration post sync so it can
committed and provided within the tarball

## Universal aggregators utils
Created two utils
### deleteUniversalFlatEntityForeignKeyAggregators
Used when building a universal create action, a newly created actions
should not contain any aggregated foreign key so they won't be codegen
in the workspace migration but also they are overriden at uninversal to
flat transpilation anw

### resetUniversalFlatEntityForeignKeyAggregators
Used before validating a new flat entity creation, some validator will
consume the fk aggregator in order to validate integrity, but of
optimstically provided it can result to errors. To avoid caller
responsability we override them here

## create-field-action refactor
Refactored the universal and flat field create action to be following
the base actions in order to ease typing
Also it was tailored to handle unlimited amount of flat field metadata
in the same actions whereas in the reality we were always only sending
at max 2 ( for relation fields )

Note: relation field has to be provided at the same as if not optimistic
would fail to retrieve circular universal identifiers

## ObjectManifest
Now always expect a `labelIdentifierFieldMetadataUniversalIdentifier`

## Integration test
Created an integration test that creates an app, sync a first manifest
and a second implying update workspace migration action generation
This commit is contained in:
Paul Rastoin
2026-02-16 15:00:24 +01:00
committed by GitHub
parent 4b3c58e013
commit 2b7b05de2e
107 changed files with 1662 additions and 566 deletions
@@ -0,0 +1,58 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should match snapshot 1`] = `
{
"agent": [],
"commandMenuItem": [],
"fieldMetadata": [
"calendarViewUniversalIdentifiers",
"kanbanAggregateOperationViewUniversalIdentifiers",
"mainGroupByFieldMetadataViewUniversalIdentifiers",
"viewFieldUniversalIdentifiers",
"viewFilterUniversalIdentifiers",
],
"frontComponent": [],
"index": [],
"logicFunction": [],
"navigationMenuItem": [],
"objectMetadata": [
"fieldUniversalIdentifiers",
"viewUniversalIdentifiers",
"indexMetadataUniversalIdentifiers",
],
"pageLayout": [
"tabUniversalIdentifiers",
],
"pageLayoutTab": [
"widgetUniversalIdentifiers",
],
"pageLayoutWidget": [],
"role": [
"roleTargetUniversalIdentifiers",
],
"roleTarget": [],
"rowLevelPermissionPredicate": [],
"rowLevelPermissionPredicateGroup": [
"childRowLevelPermissionPredicateGroupUniversalIdentifiers",
],
"skill": [],
"view": [
"viewFieldUniversalIdentifiers",
"viewFieldGroupUniversalIdentifiers",
"viewFilterUniversalIdentifiers",
"viewGroupUniversalIdentifiers",
"viewFilterGroupUniversalIdentifiers",
],
"viewField": [],
"viewFieldGroup": [
"viewFieldUniversalIdentifiers",
],
"viewFilter": [],
"viewFilterGroup": [
"viewFilterUniversalIdentifiers",
"childViewFilterGroupUniversalIdentifiers",
],
"viewGroup": [],
"webhook": [],
}
`;
@@ -0,0 +1,14 @@
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant';
describe('ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES', () => {
it('should match snapshot', () => {
expect(
Object.keys(ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES),
).toMatchObject(Object.values(ALL_METADATA_NAME));
expect(
ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES,
).toMatchSnapshot();
});
});
@@ -0,0 +1,71 @@
import {
ALL_METADATA_NAME,
type AllMetadataName,
} from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { ALL_UNIVERSAL_METADATA_RELATIONS } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-metadata-relations.constant';
type ExtractForeignKeyAggregatorFromManyToOneRelations<
ManyToOneRelations,
TargetMetadataName extends AllMetadataName,
> = {
[K in keyof ManyToOneRelations]: ManyToOneRelations[K] extends {
metadataName: TargetMetadataName;
universalFlatEntityForeignKeyAggregator: infer Agg;
}
? Agg extends string
? Agg
: never
: never;
}[keyof ManyToOneRelations];
export type ExtractUniversalForeignKeyAggregatorForMetadataName<
T extends AllMetadataName,
> = {
[M in AllMetadataName]: ExtractForeignKeyAggregatorFromManyToOneRelations<
(typeof ALL_UNIVERSAL_METADATA_RELATIONS)[M]['manyToOne'],
T
>;
}[AllMetadataName];
type UniversalFlatEntityForeignKeyAggregatorProperties = {
[P in AllMetadataName]: ExtractUniversalForeignKeyAggregatorForMetadataName<P>[];
};
const computeForeignKeyAggregatorProperties = <T extends AllMetadataName>(
metadataName: T,
): ExtractUniversalForeignKeyAggregatorForMetadataName<T>[] => {
const aggregatorProperties: ExtractUniversalForeignKeyAggregatorForMetadataName<T>[] =
[];
for (const relationsEntry of Object.values(
ALL_UNIVERSAL_METADATA_RELATIONS,
)) {
for (const relation of Object.values(relationsEntry.manyToOne)) {
if (!isDefined(relation)) {
continue;
}
if (
relation.metadataName === metadataName &&
isDefined(relation.universalFlatEntityForeignKeyAggregator)
) {
aggregatorProperties.push(
relation.universalFlatEntityForeignKeyAggregator as ExtractUniversalForeignKeyAggregatorForMetadataName<T>,
);
}
}
}
return aggregatorProperties;
};
export const ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES =
Object.values(ALL_METADATA_NAME).reduce(
(acc, metadataName) => ({
...acc,
[metadataName]: computeForeignKeyAggregatorProperties(metadataName),
}),
{} as UniversalFlatEntityForeignKeyAggregatorProperties,
);
@@ -0,0 +1,36 @@
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { deleteUniversalFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-foreign-key-aggregators.util';
describe('deleteUniversalFlatEntityForeignKeyAggregators', () => {
it('should delete all aggregator keys from the entity', () => {
const objectMetadata = getFlatObjectMetadataMock({
universalIdentifier: 'object-1',
fieldUniversalIdentifiers: ['field-1', 'field-2'],
viewUniversalIdentifiers: ['view-1'],
indexMetadataUniversalIdentifiers: ['index-1'],
});
const result = deleteUniversalFlatEntityForeignKeyAggregators({
universalFlatEntity: objectMetadata,
metadataName: 'objectMetadata',
});
expect(result).not.toHaveProperty('fieldUniversalIdentifiers');
expect(result).not.toHaveProperty('viewUniversalIdentifiers');
expect(result).not.toHaveProperty('indexMetadataUniversalIdentifiers');
expect(result.universalIdentifier).toBe('object-1');
});
it('should return the same reference for metadata with no aggregators', () => {
const entity = getFlatObjectMetadataMock({
universalIdentifier: 'logic-1',
});
const result = deleteUniversalFlatEntityForeignKeyAggregators({
universalFlatEntity: entity,
metadataName: 'logicFunction' as any,
});
expect(result).toBe(entity);
});
});
@@ -0,0 +1,50 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { resetUniversalFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
describe('resetUniversalFlatEntityForeignKeyAggregators', () => {
it('should reset aggregator properties to empty arrays for objectMetadata', () => {
const objectMetadata = getFlatObjectMetadataMock({
universalIdentifier: 'object-1',
fieldUniversalIdentifiers: ['field-1', 'field-2'],
viewUniversalIdentifiers: ['view-1'],
indexMetadataUniversalIdentifiers: ['index-1'],
});
const result = resetUniversalFlatEntityForeignKeyAggregators({
universalFlatEntity: objectMetadata,
metadataName: 'objectMetadata',
});
expect(result).toMatchObject({
fieldUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
});
});
it('should reset aggregator properties to empty arrays for fieldMetadata', () => {
const fieldMetadata = getFlatFieldMetadataMock({
universalIdentifier: 'field-1',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
viewFieldUniversalIdentifiers: ['vf-1'],
viewFilterUniversalIdentifiers: ['filter-1'],
});
const result = resetUniversalFlatEntityForeignKeyAggregators({
universalFlatEntity: fieldMetadata,
metadataName: 'fieldMetadata',
});
expect(result).toMatchObject({
viewFieldUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
});
});
});
@@ -0,0 +1,29 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
import { ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant';
export const deleteUniversalFlatEntityForeignKeyAggregators = <
T extends AllMetadataName,
>({
universalFlatEntity,
metadataName,
}: {
universalFlatEntity: MetadataUniversalFlatEntity<T>;
metadataName: T;
}): MetadataUniversalFlatEntity<T> => {
const aggregatorProperties =
ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES[metadataName];
if (aggregatorProperties.length === 0) {
return universalFlatEntity;
}
const result = { ...universalFlatEntity };
for (const aggregatorProperty of aggregatorProperties) {
delete (result as Record<string, unknown>)[aggregatorProperty];
}
return result;
};
@@ -0,0 +1,48 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
import {
ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES,
type ExtractUniversalForeignKeyAggregatorForMetadataName,
} from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant';
export const getUniversalFlatEntityEmptyForeignKeyAggregators = <
T extends AllMetadataName,
>({
metadataName,
}: {
metadataName: T;
}) => {
const aggregatorProperties =
ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES[metadataName];
const emptyAggregatorsRecord = {} as Record<
ExtractUniversalForeignKeyAggregatorForMetadataName<T>,
string[]
>;
for (const aggregatorProperty of aggregatorProperties) {
emptyAggregatorsRecord[aggregatorProperty] = [];
}
return emptyAggregatorsRecord;
};
export const resetUniversalFlatEntityForeignKeyAggregators = <
T extends AllMetadataName,
>({
universalFlatEntity,
metadataName,
}: {
universalFlatEntity: MetadataUniversalFlatEntity<T>;
metadataName: T;
}): MetadataUniversalFlatEntity<T> => {
const overrides = getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName,
});
return {
...universalFlatEntity,
...overrides,
};
};