[REQUIRES_CACHE_FLUSH_FOR_FIELD_AND_OBJECT]FlatFieldMetadata and FlatObjectMetadata required universal (#17557)

# Introduction
In this PR we're migrating both the `field` and `object` metadata to be
using the new `FlatEntityFromV2` that requires all the
`UniversalFlatEntityExtraProperties` to be spread at the flat entity
root.

This means that we have to update all of their flat declaration

This type swap allows to isole a specific entity migration into his own
type scope and avoid to have everything handled at once

```ts
/**
 * Currently under migration but aims to replace FlatEntity afterwards
 */
export type FlatEntityFromV2<
  TEntity,
  TMetadataName extends AllMetadataName | undefined = undefined,
  TInnerFlatEntity extends { __universal?: unknown } = FlatEntityFrom<
    TEntity,
    TMetadataName
  >,
> = Omit<TInnerFlatEntity, '__universal'> & TInnerFlatEntity['__universal'];

```

## Impact
Both object and field:
- Create input transpilation utils
- from entity to flat tools
- mocks

## Note
Removed from the universal extra properties the jsonb properties that do
not contain a serialized

## Next
Next step is to incrementally make the builder and runner expect
`UniversalFlatEntity` for both of these metadata
This way we will be able to fully migrate an entity e2e typesafely
This commit is contained in:
Paul Rastoin
2026-01-29 19:11:19 +01:00
committed by GitHub
parent e91457a47e
commit fbbd8fe967
69 changed files with 1244 additions and 498 deletions
@@ -132,36 +132,43 @@ export const mockPersonFlatObjectMetadataMaps = (
export const mockPersonFlatObjectMetadata = (
duplicateCriteria: WorkspaceEntityDuplicateCriteria[],
): FlatObjectMetadata =>
({
id: objectMetadataId,
icon: 'Icon123',
standardId: '',
nameSingular: 'person',
namePlural: 'people',
labelSingular: 'Person',
labelPlural: 'People',
targetTableName: 'person',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
isAuditLogged: true,
isSearchable: true,
duplicateCriteria: duplicateCriteria,
labelIdentifierFieldMetadataId: '',
imageIdentifierFieldMetadataId: '',
workspaceId,
universalIdentifier: objectMetadataId,
indexMetadataIds: [],
fieldIds: mockFieldMetadatas.map((field) => field.id),
viewIds: [],
applicationId: 'test-application-id',
isLabelSyncedWithName: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
shortcut: null,
description: null,
standardOverrides: null,
isUIReadOnly: false,
}) as FlatObjectMetadata;
): FlatObjectMetadata => ({
id: objectMetadataId,
icon: 'Icon123',
standardId: '',
nameSingular: 'person',
namePlural: 'people',
labelSingular: 'Person',
labelPlural: 'People',
targetTableName: 'person',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
isAuditLogged: true,
isSearchable: true,
duplicateCriteria: duplicateCriteria,
labelIdentifierFieldMetadataId: '',
imageIdentifierFieldMetadataId: '',
workspaceId,
universalIdentifier: objectMetadataId,
indexMetadataIds: [],
fieldIds: mockFieldMetadatas.map((field) => field.id),
viewIds: [],
applicationId: 'test-application-id',
isLabelSyncedWithName: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
shortcut: null,
description: null,
standardOverrides: null,
isUIReadOnly: false,
applicationUniversalIdentifier: 'test-application-id',
fieldUniversalIdentifiers: mockFieldMetadatas.map(
(field) => field.universalIdentifier,
),
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
imageIdentifierFieldMetadataUniversalIdentifier: null,
});
@@ -3,7 +3,7 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
const mockObjectMetadata = {
const mockObjectMetadata: FlatObjectMetadata = {
id: '1',
icon: 'Icon123',
nameSingular: 'Object',
@@ -34,7 +34,13 @@ const mockObjectMetadata = {
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
duplicateCriteria: null,
} as FlatObjectMetadata;
applicationUniversalIdentifier: 'test-application-id',
fieldUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
imageIdentifierFieldMetadataUniversalIdentifier: null,
};
const mockFlatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata> = {
byId: {},
@@ -14,6 +14,7 @@ export const FieldMetadataExceptionCode = appendCommonExceptionCode({
FIELD_MUTATION_NOT_ALLOWED: 'FIELD_MUTATION_NOT_ALLOWED',
FIELD_ALREADY_EXISTS: 'FIELD_ALREADY_EXISTS',
OBJECT_METADATA_NOT_FOUND: 'OBJECT_METADATA_NOT_FOUND',
APPLICATION_NOT_FOUND: 'APPLICATION_NOT_FOUND',
FIELD_METADATA_RELATION_NOT_ENABLED: 'FIELD_METADATA_RELATION_NOT_ENABLED',
FIELD_METADATA_RELATION_MALFORMED: 'FIELD_METADATA_RELATION_MALFORMED',
LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND:
@@ -43,6 +44,8 @@ const getFieldMetadataExceptionUserFriendlyMessage = (
return msg`A field with this name already exists.`;
case FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND:
return msg`Object not found.`;
case FieldMetadataExceptionCode.APPLICATION_NOT_FOUND:
return msg`Application not found.`;
case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_NOT_ENABLED:
return msg`Relation is not enabled for this field.`;
case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED:
@@ -31,6 +31,7 @@ import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { FieldMetadataEntity } from './field-metadata.entity';
@@ -62,6 +63,7 @@ import { UpdateFieldInput } from './dtos/update-field.input';
FlatFieldMetadataModule,
IndexMetadataModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceCacheModule,
],
services: [FieldMetadataService],
resolvers: [
@@ -22,6 +22,7 @@ import { fromCreateFieldInputToFlatFieldMetadatasToCreate } from 'src/engine/met
import { fromDeleteFieldInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util';
import { fromUpdateFieldInputToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util';
import { throwOnFieldInputTranspilationsError } from 'src/engine/metadata-modules/flat-field-metadata/utils/throw-on-field-input-transpilations-error.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { EMPTY_ORCHESTRATOR_FAILURE_REPORT } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-failure-report.constant';
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';
@@ -34,6 +35,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly applicationService: ApplicationService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {
super(fieldMetadataRepository);
}
@@ -180,7 +182,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
flatViewGroupMaps: existingFlatViewGroupMaps,
flatViewMaps: existingFlatViewMaps,
flatViewFieldMaps: existingFlatViewFieldMaps,
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
flatApplication: workspaceCustomFlatApplication,
isSystemBuild,
});
@@ -309,13 +311,26 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
},
);
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps'],
},
const {
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatApplicationMaps',
]);
const ownerFlatApplication = isDefined(applicationId)
? flatApplicationMaps.byId[applicationId]
: workspaceCustomFlatApplication;
if (!isDefined(ownerFlatApplication)) {
throw new FieldMetadataException(
`Could not find related application ${applicationId}`,
FieldMetadataExceptionCode.APPLICATION_NOT_FOUND,
);
}
const allTranspiledTranspilationInputs: Awaited<
ReturnType<typeof fromCreateFieldInputToFlatFieldMetadatasToCreate>
@@ -325,10 +340,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
allTranspiledTranspilationInputs.push(
await fromCreateFieldInputToFlatFieldMetadatasToCreate({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
createFieldInput,
workspaceId,
workspaceCustomApplicationId:
applicationId ?? workspaceCustomFlatApplication.id,
flatApplication: ownerFlatApplication,
}),
);
}
@@ -38,6 +38,7 @@ export const fieldMetadataGraphqlApiExceptionHandler = (
case FieldMetadataExceptionCode.FIELD_ALREADY_EXISTS:
throw new ConflictError(error);
case FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND:
case FieldMetadataExceptionCode.APPLICATION_NOT_FOUND:
case FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR:
case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_NOT_ENABLED:
case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED:
@@ -43,8 +43,6 @@ export const transformAgentEntityToFlatAgent = ({
__universal: {
universalIdentifier: agentEntity.universalIdentifier,
applicationUniversalIdentifier,
responseFormat: agentEntity.responseFormat,
modelConfiguration: agentEntity.modelConfiguration,
},
};
};
@@ -4,6 +4,7 @@ import { type Expect } from 'twenty-shared/testing';
import { type AllFlatEntityTypesByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type WorkspaceMigrationActionType } from 'src/engine/metadata-modules/flat-entity/types/metadata-workspace-migration-action.type';
import { type UniversalSyncableFlatEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
import { type WorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
type ExpectedGenericFlatEntityInformation = {
@@ -13,6 +14,7 @@ type ExpectedGenericFlatEntityInformation = {
| WorkspaceMigrationAction[];
};
flatEntity: SyncableFlatEntity;
universalFlatEntity: UniversalSyncableFlatEntity;
};
type ExpectedGenericAllFlatEntityInformationByMetadataEngine = {
@@ -1,10 +1,12 @@
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type';
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
@@ -18,10 +20,30 @@ import { type FlatViewFilterGroup } from 'src/engine/metadata-modules/flat-view-
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type';
import { type FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { type IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { type LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { type NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { type PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { type PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { type FlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group.type';
import { type FlatRowLevelPermissionPredicate } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate.type';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { type RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
import { type RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { type SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { type ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { type ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { type WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
import {
type CreateAgentAction,
type DeleteAgentAction,
@@ -141,6 +163,10 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteFieldAction;
};
flatEntity: FlatFieldMetadata;
universalFlatEntity: UniversalFlatEntityFrom<
FieldMetadataEntity,
'fieldMetadata'
>;
entity: MetadataEntity<'fieldMetadata'>;
};
objectMetadata: {
@@ -150,6 +176,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteObjectAction;
};
flatEntity: FlatObjectMetadata;
universalFlatEntity: UniversalFlatEntityFrom<ObjectMetadataEntity>;
entity: MetadataEntity<'objectMetadata'>;
};
view: {
@@ -159,6 +186,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteViewAction;
};
flatEntity: FlatView;
universalFlatEntity: UniversalFlatEntityFrom<ViewEntity>;
entity: MetadataEntity<'view'>;
};
viewField: {
@@ -168,6 +196,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteViewFieldAction;
};
flatEntity: FlatViewField;
universalFlatEntity: UniversalFlatEntityFrom<ViewFieldEntity>;
entity: MetadataEntity<'viewField'>;
};
viewGroup: {
@@ -177,6 +206,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteViewGroupAction;
};
flatEntity: FlatViewGroup;
universalFlatEntity: UniversalFlatEntityFrom<ViewGroupEntity>;
entity: MetadataEntity<'viewGroup'>;
};
rowLevelPermissionPredicate: {
@@ -186,6 +216,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteRowLevelPermissionPredicateAction;
};
flatEntity: FlatRowLevelPermissionPredicate;
universalFlatEntity: UniversalFlatEntityFrom<RowLevelPermissionPredicateEntity>;
entity: MetadataEntity<'rowLevelPermissionPredicate'>;
};
rowLevelPermissionPredicateGroup: {
@@ -195,6 +226,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteRowLevelPermissionPredicateGroupAction;
};
flatEntity: FlatRowLevelPermissionPredicateGroup;
universalFlatEntity: UniversalFlatEntityFrom<RowLevelPermissionPredicateGroupEntity>;
entity: MetadataEntity<'rowLevelPermissionPredicateGroup'>;
};
viewFilterGroup: {
@@ -204,6 +236,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteViewFilterGroupAction;
};
flatEntity: FlatViewFilterGroup;
universalFlatEntity: UniversalFlatEntityFrom<ViewFilterGroupEntity>;
entity: MetadataEntity<'viewFilterGroup'>;
};
index: {
@@ -213,6 +246,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteIndexAction;
};
flatEntity: FlatIndexMetadata;
universalFlatEntity: UniversalFlatEntityFrom<IndexMetadataEntity>;
entity: MetadataEntity<'index'>;
};
logicFunction: {
@@ -222,6 +256,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteLogicFunctionAction;
};
flatEntity: FlatLogicFunction;
universalFlatEntity: UniversalFlatEntityFrom<LogicFunctionEntity>;
entity: MetadataEntity<'logicFunction'>;
};
viewFilter: {
@@ -231,6 +266,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteViewFilterAction;
};
flatEntity: FlatViewFilter;
universalFlatEntity: UniversalFlatEntityFrom<ViewFilterEntity>;
entity: MetadataEntity<'viewFilter'>;
};
role: {
@@ -240,6 +276,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteRoleAction;
};
flatEntity: FlatRole;
universalFlatEntity: UniversalFlatEntityFrom<RoleEntity>;
entity: MetadataEntity<'role'>;
};
roleTarget: {
@@ -249,6 +286,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteRoleTargetAction;
};
flatEntity: FlatRoleTarget;
universalFlatEntity: UniversalFlatEntityFrom<RoleTargetEntity>;
entity: MetadataEntity<'roleTarget'>;
};
agent: {
@@ -258,6 +296,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteAgentAction;
};
flatEntity: FlatAgent;
universalFlatEntity: UniversalFlatEntityFrom<AgentEntity>;
entity: MetadataEntity<'agent'>;
};
skill: {
@@ -267,6 +306,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteSkillAction;
};
flatEntity: FlatSkill;
universalFlatEntity: UniversalFlatEntityFrom<SkillEntity>;
entity: MetadataEntity<'skill'>;
};
commandMenuItem: {
@@ -276,6 +316,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteCommandMenuItemAction;
};
flatEntity: FlatCommandMenuItem;
universalFlatEntity: UniversalFlatEntityFrom<CommandMenuItemEntity>;
entity: MetadataEntity<'commandMenuItem'>;
};
navigationMenuItem: {
@@ -285,6 +326,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteNavigationMenuItemAction;
};
flatEntity: FlatNavigationMenuItem;
universalFlatEntity: UniversalFlatEntityFrom<NavigationMenuItemEntity>;
entity: NavigationMenuItemEntity;
};
pageLayout: {
@@ -294,6 +336,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeletePageLayoutAction;
};
flatEntity: FlatPageLayout;
universalFlatEntity: UniversalFlatEntityFrom<PageLayoutEntity>;
entity: MetadataEntity<'pageLayout'>;
};
pageLayoutWidget: {
@@ -303,6 +346,10 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeletePageLayoutWidgetAction;
};
flatEntity: FlatPageLayoutWidget;
universalFlatEntity: UniversalFlatEntityFrom<
PageLayoutWidgetEntity,
'pageLayoutWidget'
>;
entity: MetadataEntity<'pageLayoutWidget'>;
};
pageLayoutTab: {
@@ -312,6 +359,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeletePageLayoutTabAction;
};
flatEntity: FlatPageLayoutTab;
universalFlatEntity: UniversalFlatEntityFrom<PageLayoutTabEntity>;
entity: MetadataEntity<'pageLayoutTab'>;
};
frontComponent: {
@@ -321,6 +369,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteFrontComponentAction;
};
flatEntity: FlatFrontComponent;
universalFlatEntity: UniversalFlatEntityFrom<FrontComponentEntity>;
entity: MetadataEntity<'frontComponent'>;
};
webhook: {
@@ -330,6 +379,7 @@ export type AllFlatEntityTypesByMetadataName = {
delete: DeleteWebhookAction;
};
flatEntity: FlatWebhook;
universalFlatEntity: UniversalFlatEntityFrom<WebhookEntity>;
entity: MetadataEntity<'webhook'>;
};
};
@@ -0,0 +1,15 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
/**
* Currently under migration but aims to replace FlatEntity afterwards
*/
export type FlatEntityFromV2<
TEntity,
TMetadataName extends AllMetadataName | undefined = undefined,
TInnerFlatEntity extends { __universal?: unknown } = FlatEntityFrom<
TEntity,
TMetadataName
>,
> = Omit<TInnerFlatEntity, '__universal'> & TInnerFlatEntity['__universal'];
@@ -16,6 +16,7 @@ export type SyncableFlatEntity = Omit<
export type FlatEntityFrom<
TEntity,
// Required to be passed for narrowed type
TMetadataName extends AllMetadataName | undefined = undefined,
> = Omit<
TEntity,
@@ -0,0 +1,6 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type AllFlatEntityTypesByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name';
export type MetadataUniversalFlatEntity<T extends AllMetadataName> =
AllFlatEntityTypesByMetadataName[T]['universalFlatEntity'];
@@ -45,8 +45,16 @@ 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,
applicationUniversalIdentifier: faker.string.uuid(),
objectMetadataUniversalIdentifier: faker.string.uuid(),
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
...overrides,
};
};
@@ -65,6 +65,15 @@ export const getRelationTargetFlatFieldMetadataMock = ({
defaultValue: null,
options: null,
applicationId: faker.string.uuid(),
__universal: undefined,
applicationUniversalIdentifier: faker.string.uuid(),
objectMetadataUniversalIdentifier: faker.string.uuid(),
relationTargetObjectMetadataUniversalIdentifier: faker.string.uuid(),
relationTargetFieldMetadataUniversalIdentifier: faker.string.uuid(),
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: settings,
};
};
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test suite Failure cases should fail when morphRelationsCreationPayload has different relation types 1`] = `
{
@@ -117,7 +117,9 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"flatFieldMetadatas": [
{
"applicationId": Any<String>,
"applicationUniversalIdentifier": Any<String>,
"calendarViewIds": [],
"calendarViewUniversalIdentifiers": [],
"createdAt": Any<String>,
"defaultValue": null,
"description": "new field description",
@@ -131,14 +133,19 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
"label": "newFieldLabel",
"mainGroupByFieldMetadataViewIds": [],
"mainGroupByFieldMetadataViewUniversalIdentifiers": [],
"morphId": Any<String>,
"name": "newFieldPets",
"objectMetadataId": Any<String>,
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataId": Any<String>,
"relationTargetFieldMetadataUniversalIdentifier": Any<String>,
"relationTargetObjectMetadataId": Any<String>,
"relationTargetObjectMetadataUniversalIdentifier": Any<String>,
"settings": {
"relationType": "ONE_TO_MANY",
},
@@ -146,14 +153,21 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"standardOverrides": null,
"type": "MORPH_RELATION",
"universalIdentifier": Any<String>,
"universalSettings": {
"relationType": "ONE_TO_MANY",
},
"updatedAt": Any<String>,
"viewFieldIds": [],
"viewFieldUniversalIdentifiers": [],
"viewFilterIds": [],
"viewFilterUniversalIdentifiers": [],
"workspaceId": Any<String>,
},
{
"applicationId": Any<String>,
"applicationUniversalIdentifier": Any<String>,
"calendarViewIds": [],
"calendarViewUniversalIdentifiers": [],
"createdAt": Any<String>,
"defaultValue": null,
"description": "new field description",
@@ -167,14 +181,19 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
"label": "Pet",
"mainGroupByFieldMetadataViewIds": [],
"mainGroupByFieldMetadataViewUniversalIdentifiers": [],
"morphId": null,
"name": "pet",
"objectMetadataId": Any<String>,
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataId": Any<String>,
"relationTargetFieldMetadataUniversalIdentifier": Any<String>,
"relationTargetObjectMetadataId": Any<String>,
"relationTargetObjectMetadataUniversalIdentifier": Any<String>,
"settings": {
"joinColumnName": "petId",
"onDelete": "SET_NULL",
@@ -184,14 +203,23 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"standardOverrides": null,
"type": "RELATION",
"universalIdentifier": Any<String>,
"universalSettings": {
"joinColumnName": "petId",
"onDelete": "SET_NULL",
"relationType": "MANY_TO_ONE",
},
"updatedAt": Any<String>,
"viewFieldIds": [],
"viewFieldUniversalIdentifiers": [],
"viewFilterIds": [],
"viewFilterUniversalIdentifiers": [],
"workspaceId": Any<String>,
},
{
"applicationId": Any<String>,
"applicationUniversalIdentifier": Any<String>,
"calendarViewIds": [],
"calendarViewUniversalIdentifiers": [],
"createdAt": Any<String>,
"defaultValue": null,
"description": "new field description",
@@ -205,14 +233,19 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
"label": "newFieldLabel",
"mainGroupByFieldMetadataViewIds": [],
"mainGroupByFieldMetadataViewUniversalIdentifiers": [],
"morphId": Any<String>,
"name": "newFieldCompanies",
"objectMetadataId": Any<String>,
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataId": Any<String>,
"relationTargetFieldMetadataUniversalIdentifier": Any<String>,
"relationTargetObjectMetadataId": Any<String>,
"relationTargetObjectMetadataUniversalIdentifier": Any<String>,
"settings": {
"relationType": "ONE_TO_MANY",
},
@@ -220,14 +253,21 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"standardOverrides": null,
"type": "MORPH_RELATION",
"universalIdentifier": Any<String>,
"universalSettings": {
"relationType": "ONE_TO_MANY",
},
"updatedAt": Any<String>,
"viewFieldIds": [],
"viewFieldUniversalIdentifiers": [],
"viewFilterIds": [],
"viewFilterUniversalIdentifiers": [],
"workspaceId": Any<String>,
},
{
"applicationId": Any<String>,
"applicationUniversalIdentifier": Any<String>,
"calendarViewIds": [],
"calendarViewUniversalIdentifiers": [],
"createdAt": Any<String>,
"defaultValue": null,
"description": "new field description",
@@ -241,14 +281,19 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
"label": "Company",
"mainGroupByFieldMetadataViewIds": [],
"mainGroupByFieldMetadataViewUniversalIdentifiers": [],
"morphId": null,
"name": "company",
"objectMetadataId": Any<String>,
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataId": Any<String>,
"relationTargetFieldMetadataUniversalIdentifier": Any<String>,
"relationTargetObjectMetadataId": Any<String>,
"relationTargetObjectMetadataUniversalIdentifier": Any<String>,
"settings": {
"joinColumnName": "companyId",
"onDelete": "SET_NULL",
@@ -258,9 +303,16 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"standardOverrides": null,
"type": "RELATION",
"universalIdentifier": Any<String>,
"universalSettings": {
"joinColumnName": "companyId",
"onDelete": "SET_NULL",
"relationType": "MANY_TO_ONE",
},
"updatedAt": Any<String>,
"viewFieldIds": [],
"viewFieldUniversalIdentifiers": [],
"viewFilterIds": [],
"viewFilterUniversalIdentifiers": [],
"workspaceId": Any<String>,
},
],
@@ -5,8 +5,11 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
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 { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import {
fromCreateFieldInputToFlatFieldMetadatasToCreate,
type FromCreateFieldInputToFlatObjectMetadataArgs,
@@ -16,6 +19,24 @@ import { FLAT_OBJECT_METADATA_MAPS_MOCKS } from 'src/engine/metadata-modules/fla
import { PET_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/pet-flat-object.mock';
import { ROCKET_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/rocket-flat-object.mock';
const MOCK_FLAT_APPLICATION: FlatApplication = {
id: '20202020-81ee-42da-a281-668632f32fe7',
universalIdentifier: '20202020-81ee-42da-a281-668632f32fe7',
workspaceId: 'mock-workspace-id',
name: 'Workspace Custom Application',
description: null,
version: null,
sourceType: 'local',
sourcePath: '',
logicFunctionLayerId: null,
defaultLogicFunctionRoleId: null,
defaultLogicFunctionRole: null,
canBeUninstalled: false,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
};
const flatObjectMetadataMaps = [
COMPANY_FLAT_OBJECT_MOCK,
ROCKET_FLAT_OBJECT_MOCK,
@@ -27,6 +48,9 @@ const flatObjectMetadataMaps = [
});
}, createEmptyFlatEntityMaps());
const emptyFlatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata> =
createEmptyFlatEntityMaps();
type TestCase = EachTestingContext<{
input: FromCreateFieldInputToFlatObjectMetadataArgs;
expected: 'success' | 'fail';
@@ -42,8 +66,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
'should create morph relation field metadata with valid input on rocket object to pet object',
context: {
input: {
workspaceCustomApplicationId:
'20202020-81ee-42da-a281-668632f32fe7',
flatApplication: MOCK_FLAT_APPLICATION,
createFieldInput: {
name: 'newField',
label: 'newFieldLabel',
@@ -67,6 +90,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
},
workspaceId: mockWorkspaceId,
flatObjectMetadataMaps,
flatFieldMetadataMaps: emptyFlatFieldMetadataMaps,
},
expected: 'success',
},
@@ -102,8 +126,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
title: 'should fail when morphRelationsCreationPayload is missing',
context: {
input: {
workspaceCustomApplicationId:
'20202020-81ee-42da-a281-668632f32fe7',
flatApplication: MOCK_FLAT_APPLICATION,
createFieldInput: {
name: 'newField',
label: 'newFieldLabel',
@@ -114,6 +137,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
},
workspaceId: mockWorkspaceId,
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
flatFieldMetadataMaps: emptyFlatFieldMetadataMaps,
},
expected: 'fail',
},
@@ -122,8 +146,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
title: 'should fail when morphRelationsCreationPayload is empty array',
context: {
input: {
workspaceCustomApplicationId:
'20202020-81ee-42da-a281-668632f32fe7',
flatApplication: MOCK_FLAT_APPLICATION,
createFieldInput: {
name: 'newField',
label: 'newFieldLabel',
@@ -134,6 +157,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
},
workspaceId: mockWorkspaceId,
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
flatFieldMetadataMaps: emptyFlatFieldMetadataMaps,
},
expected: 'fail',
},
@@ -143,8 +167,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
'should fail when morphRelationsCreationPayload has different relation types',
context: {
input: {
workspaceCustomApplicationId:
'20202020-81ee-42da-a281-668632f32fe7',
flatApplication: MOCK_FLAT_APPLICATION,
createFieldInput: {
name: 'newField',
label: 'newFieldLabel',
@@ -168,6 +191,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
},
workspaceId: mockWorkspaceId,
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
flatFieldMetadataMaps: emptyFlatFieldMetadataMaps,
},
expected: 'fail',
},
@@ -177,8 +201,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
'should fail when morphRelationsCreationPayload has several references to same object metadata',
context: {
input: {
workspaceCustomApplicationId:
'20202020-81ee-42da-a281-668632f32fe7',
flatApplication: MOCK_FLAT_APPLICATION,
createFieldInput: {
name: 'newField',
label: 'newFieldLabel',
@@ -202,6 +225,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
},
workspaceId: mockWorkspaceId,
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
flatFieldMetadataMaps: emptyFlatFieldMetadataMaps,
},
expected: 'fail',
},
@@ -211,8 +235,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
'should fail when morphRelationsCreationPayload has invalid relation payload',
context: {
input: {
workspaceCustomApplicationId:
'20202020-81ee-42da-a281-668632f32fe7',
flatApplication: MOCK_FLAT_APPLICATION,
createFieldInput: {
name: 'newField',
label: 'newFieldLabel',
@@ -229,6 +252,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
},
workspaceId: mockWorkspaceId,
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
flatFieldMetadataMaps: emptyFlatFieldMetadataMaps,
},
expected: 'fail',
},
@@ -237,8 +261,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
title: 'should fail when target object metadata is not found',
context: {
input: {
workspaceCustomApplicationId:
'20202020-81ee-42da-a281-668632f32fe7',
flatApplication: MOCK_FLAT_APPLICATION,
createFieldInput: {
name: 'newField',
label: 'newFieldLabel',
@@ -256,6 +279,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s
},
workspaceId: mockWorkspaceId,
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
flatFieldMetadataMaps: emptyFlatFieldMetadataMaps,
},
expected: 'fail',
},
@@ -71,3 +71,48 @@ type FlatTransformationAssertions = [
Expect<HasAllProperties<FlatFieldMetadata, DatePropertiesCastToString>>,
Expect<HasAllProperties<FlatFieldMetadata, OneToManyRelationIdArrays>>,
];
// Any narrowed flatFieldMetadata type should be assignable to non narrowed flatFieldMetadata
type AbstractFlatFieldMetadata = FlatFieldMetadata<FieldMetadataType>;
const _assertion: Record<string, AbstractFlatFieldMetadata> = {
// Primitive types
uuid: {} as FlatFieldMetadata<FieldMetadataType.UUID>,
text: {} as FlatFieldMetadata<FieldMetadataType.TEXT>,
number: {} as FlatFieldMetadata<FieldMetadataType.NUMBER>,
boolean: {} as FlatFieldMetadata<FieldMetadataType.BOOLEAN>,
numeric: {} as FlatFieldMetadata<FieldMetadataType.NUMERIC>,
position: {} as FlatFieldMetadata<FieldMetadataType.POSITION>,
// Date types
date: {} as FlatFieldMetadata<FieldMetadataType.DATE>,
dateTime: {} as FlatFieldMetadata<FieldMetadataType.DATE_TIME>,
// Complex types
currency: {} as FlatFieldMetadata<FieldMetadataType.CURRENCY>,
fullName: {} as FlatFieldMetadata<FieldMetadataType.FULL_NAME>,
address: {} as FlatFieldMetadata<FieldMetadataType.ADDRESS>,
links: {} as FlatFieldMetadata<FieldMetadataType.LINKS>,
emails: {} as FlatFieldMetadata<FieldMetadataType.EMAILS>,
phones: {} as FlatFieldMetadata<FieldMetadataType.PHONES>,
actor: {} as FlatFieldMetadata<FieldMetadataType.ACTOR>,
// Select types
rating: {} as FlatFieldMetadata<FieldMetadataType.RATING>,
select: {} as FlatFieldMetadata<FieldMetadataType.SELECT>,
multiSelect: {} as FlatFieldMetadata<FieldMetadataType.MULTI_SELECT>,
// JSON/Array types
rawJson: {} as FlatFieldMetadata<FieldMetadataType.RAW_JSON>,
array: {} as FlatFieldMetadata<FieldMetadataType.ARRAY>,
richText: {} as FlatFieldMetadata<FieldMetadataType.RICH_TEXT>,
richTextV2: {} as FlatFieldMetadata<FieldMetadataType.RICH_TEXT_V2>,
// Relation types
relation: {} as FlatFieldMetadata<FieldMetadataType.RELATION>,
morphRelation: {} as FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>,
// Special types
files: {} as FlatFieldMetadata<FieldMetadataType.FILES>,
tsVector: {} as FlatFieldMetadata<FieldMetadataType.TS_VECTOR>,
};
@@ -1,10 +1,10 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityFromV2 } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from-v2.type';
export type FlatFieldMetadata<T extends FieldMetadataType = FieldMetadataType> =
FlatEntityFrom<
FlatEntityFromV2<
Omit<
FieldMetadataEntity<T>,
'relationTargetFieldMetadata' | 'relationTargetObjectMetadata'
@@ -6,6 +6,7 @@ import {
RelationType,
} from 'twenty-shared/types';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import {
generateMorphOrRelationFlatFieldMetadataPair,
type SourceTargetMorphOrRelationFlatFieldAndFlatIndex,
@@ -13,6 +14,24 @@ import {
import { COMPANY_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/company-flat-object.mock';
import { PET_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/pet-flat-object.mock';
const MOCK_FLAT_APPLICATION: FlatApplication = {
id: '20202020-81ee-42da-a281-668632f32fe7',
universalIdentifier: '20202020-81ee-42da-a281-668632f32fe7',
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
name: 'Workspace Custom Application',
description: null,
version: null,
sourceType: 'local',
sourcePath: '',
logicFunctionLayerId: null,
defaultLogicFunctionRoleId: null,
defaultLogicFunctionRole: null,
canBeUninstalled: false,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
};
type GenerateMorphOrRelationFlatFieldMetadataPairTestInput = Parameters<
typeof generateMorphOrRelationFlatFieldMetadataPair
>[0];
@@ -28,8 +47,6 @@ type TestCase = EachTestingContext<{
describe('generate Morph Or Relation Flat Field Metadata Pair test suite', () => {
const mockWorkspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419';
const mockWorkspaceCustomApplicationId =
'20202020-81ee-42da-a281-668632f32fe7';
describe('Success cases', () => {
const testCases: TestCase[] = [
@@ -43,7 +60,7 @@ describe('generate Morph Or Relation Flat Field Metadata Pair test suite', () =>
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
sourceFlatObjectMetadataJoinColumnName: 'petId',
workspaceId: mockWorkspaceId,
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
flatApplication: MOCK_FLAT_APPLICATION,
createFieldInput: {
name: 'pets',
label: 'Pets',
@@ -79,7 +96,7 @@ describe('generate Morph Or Relation Flat Field Metadata Pair test suite', () =>
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
sourceFlatObjectMetadataJoinColumnName: 'petId',
workspaceId: mockWorkspaceId,
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
flatApplication: MOCK_FLAT_APPLICATION,
createFieldInput: {
name: 'pets',
label: 'Pets',
@@ -115,7 +132,7 @@ describe('generate Morph Or Relation Flat Field Metadata Pair test suite', () =>
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
sourceFlatObjectMetadataJoinColumnName: 'petId',
workspaceId: mockWorkspaceId,
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
flatApplication: MOCK_FLAT_APPLICATION,
createFieldInput: {
name: 'pets',
label: 'Pets',
@@ -151,7 +168,7 @@ describe('generate Morph Or Relation Flat Field Metadata Pair test suite', () =>
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
sourceFlatObjectMetadataJoinColumnName: 'targetPetId',
workspaceId: mockWorkspaceId,
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
flatApplication: MOCK_FLAT_APPLICATION,
morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1',
createFieldInput: {
name: 'targetPet',
@@ -188,7 +205,7 @@ describe('generate Morph Or Relation Flat Field Metadata Pair test suite', () =>
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
sourceFlatObjectMetadataJoinColumnName: 'targetPetId',
workspaceId: mockWorkspaceId,
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
flatApplication: MOCK_FLAT_APPLICATION,
morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1',
createFieldInput: {
name: 'targetPet',
@@ -225,7 +242,7 @@ describe('generate Morph Or Relation Flat Field Metadata Pair test suite', () =>
targetFlatFieldMetadataType: FieldMetadataType.MORPH_RELATION,
sourceFlatObjectMetadataJoinColumnName: 'targetPetId',
workspaceId: mockWorkspaceId,
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
flatApplication: MOCK_FLAT_APPLICATION,
morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1',
createFieldInput: {
name: 'targetPet',
@@ -4,17 +4,19 @@ import {
} from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { extractJunctionTargetSettingsFromSettings } from 'src/engine/metadata-modules/flat-field-metadata/utils/extract-junction-target-settings-from-settings.util';
import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util';
type ComputeFlatFieldToUpdateFromMorphRelationUpdatePayloadArgs = {
workspaceCustomApplicationId: string;
flatApplication: FlatApplication;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
morphRelationsUpdatePayload?: RelationUpdatePayload[];
fieldMetadataToUpdate: FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>;
@@ -22,7 +24,7 @@ type ComputeFlatFieldToUpdateFromMorphRelationUpdatePayloadArgs = {
};
export const computeFlatFieldToUpdateFromMorphRelationUpdatePayload = ({
workspaceCustomApplicationId,
flatApplication,
flatFieldMetadataMaps,
morphRelationsUpdatePayload,
fieldMetadataToUpdate,
@@ -68,6 +70,13 @@ export const computeFlatFieldToUpdateFromMorphRelationUpdatePayload = ({
const commonTargetFieldName = initialTargetFieldMetadata.name;
const commonObjectMetadataId = initialTargetFieldMetadata.objectMetadataId;
const { junctionTargetFieldId } = extractJunctionTargetSettingsFromSettings(
fieldMetadataToUpdate.settings,
);
const junctionTargetFlatFieldMetadata = isDefined(junctionTargetFieldId)
? flatFieldMetadataMaps.byId[junctionTargetFieldId]
: undefined;
morphRelationsUpdatePayload.forEach((morphRelationUpdatePayload) => {
const { targetObjectMetadataId } = morphRelationUpdatePayload;
@@ -101,13 +110,14 @@ export const computeFlatFieldToUpdateFromMorphRelationUpdatePayload = ({
targetFlatObjectMetadata: newTargetObjectMetadata,
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
workspaceId: fieldMetadataToUpdate.workspaceId,
workspaceCustomApplicationId,
flatApplication,
sourceFlatObjectMetadataJoinColumnName:
computeMorphOrRelationFieldJoinColumnName({
name: computedMorphName,
}),
morphId: fieldMetadataToUpdate.morphId,
targetFieldName: commonTargetFieldName,
junctionTargetFlatFieldMetadata,
});
flatFieldMetadatasToCreate.push(...flatFieldMetadatas);
@@ -5,6 +5,7 @@ type JunctionTargetSettings = {
junctionTargetFieldId?: string;
};
// TODO refactor using either type predicate or FieldMetadataType discriminating union
// Extracts junction target settings from untyped settings input
// This function handles the boundary where settings come from external API input
export const extractJunctionTargetSettingsFromSettings = (
@@ -10,6 +10,7 @@ import {
} from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { generateRatingOptions } from 'src/engine/metadata-modules/field-metadata/utils/generate-rating-optionts.util';
@@ -25,14 +26,15 @@ import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-m
export type FromCreateFieldInputToFlatObjectMetadataArgs = {
createFieldInput: Omit<CreateFieldInput, 'workspaceId'>;
workspaceId: string;
workspaceCustomApplicationId: string;
} & Pick<AllFlatEntityMaps, 'flatObjectMetadataMaps'>;
flatApplication: FlatApplication;
} & Pick<AllFlatEntityMaps, 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps'>;
export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
createFieldInput: rawCreateFieldInput,
workspaceId,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
workspaceCustomApplicationId,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
flatApplication,
}: FromCreateFieldInputToFlatObjectMetadataArgs): Promise<
FieldInputTranspilationResult<{
flatFieldMetadatas: FlatFieldMetadata[];
@@ -77,7 +79,9 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
createFieldInput,
workspaceId,
fieldMetadataId,
workspaceCustomApplicationId,
flatApplication,
objectMetadataUniversalIdentifier:
parentFlatObjectMetadata.universalIdentifier,
});
switch (createFieldInput.type) {
@@ -88,21 +92,23 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
type: createFieldInput.type,
},
existingFlatObjectMetadataMaps,
existingFlatFieldMetadataMaps,
sourceFlatObjectMetadata: parentFlatObjectMetadata,
workspaceId,
workspaceCustomApplicationId,
flatApplication,
});
}
case FieldMetadataType.RELATION: {
return await fromRelationCreateFieldInputToFlatFieldMetadatas({
existingFlatObjectMetadataMaps,
existingFlatFieldMetadataMaps,
sourceFlatObjectMetadata: parentFlatObjectMetadata,
createFieldInput: {
...createFieldInput,
type: createFieldInput.type,
},
workspaceId,
workspaceCustomApplicationId,
flatApplication,
});
}
case FieldMetadataType.RATING: {
@@ -106,33 +106,27 @@ 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(
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier,
relationTargetFieldMetadataUniversalIdentifier,
viewFieldUniversalIdentifiers: fieldMetadataEntity.viewFields.map(
({ universalIdentifier }) => universalIdentifier,
),
viewFilterUniversalIdentifiers: fieldMetadataEntity.viewFilters.map(
({ universalIdentifier }) => universalIdentifier,
),
kanbanAggregateOperationViewUniversalIdentifiers:
fieldMetadataEntity.kanbanAggregateOperationViews.map(
({ universalIdentifier }) => universalIdentifier,
),
viewFilterUniversalIdentifiers: fieldMetadataEntity.viewFilters.map(
calendarViewUniversalIdentifiers: fieldMetadataEntity.calendarViews.map(
({ universalIdentifier }) => universalIdentifier,
),
mainGroupByFieldMetadataViewUniversalIdentifiers:
fieldMetadataEntity.mainGroupByFieldMetadataViews?.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,
},
) ?? [],
universalSettings: settingsWithUniversalIdentifiers,
};
};
@@ -3,12 +3,14 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { extractJunctionTargetSettingsFromSettings } from 'src/engine/metadata-modules/flat-field-metadata/utils/extract-junction-target-settings-from-settings.util';
import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util';
import { validateMorphRelationCreationPayload } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-creation-payload.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
@@ -19,16 +21,18 @@ type FromMorphRelationCreateFieldInputToFlatFieldMetadatasArgs = {
type: FieldMetadataType.MORPH_RELATION;
};
existingFlatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
existingFlatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
sourceFlatObjectMetadata: FlatObjectMetadata;
workspaceId: string;
workspaceCustomApplicationId: string;
flatApplication: FlatApplication;
};
export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
createFieldInput,
existingFlatObjectMetadataMaps,
existingFlatFieldMetadataMaps,
sourceFlatObjectMetadata,
workspaceId,
workspaceCustomApplicationId,
flatApplication,
}: FromMorphRelationCreateFieldInputToFlatFieldMetadatasArgs): Promise<
FieldInputTranspilationResult<{
flatFieldMetadatas: FlatFieldMetadata[];
@@ -66,6 +70,13 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
return morphRelationCreationPayloadValidation;
}
const { junctionTargetFieldId } = extractJunctionTargetSettingsFromSettings(
createFieldInput.settings,
);
const junctionTargetFlatFieldMetadata = isDefined(junctionTargetFieldId)
? existingFlatFieldMetadataMaps.byId[junctionTargetFieldId]
: undefined;
const morphRelationCreationPayload =
morphRelationCreationPayloadValidation.result;
const morphId = v4();
@@ -95,7 +106,8 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
workspaceId,
morphId,
workspaceCustomApplicationId,
flatApplication,
junctionTargetFlatFieldMetadata,
});
return {
@@ -2,11 +2,14 @@ import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { extractJunctionTargetSettingsFromSettings } from 'src/engine/metadata-modules/flat-field-metadata/utils/extract-junction-target-settings-from-settings.util';
import {
generateMorphOrRelationFlatFieldMetadataPair,
type SourceTargetMorphOrRelationFlatFieldAndFlatIndex,
@@ -19,16 +22,18 @@ type FromRelationCreateFieldInputToFlatFieldMetadataArgs = {
type: FieldMetadataType.RELATION;
};
existingFlatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
existingFlatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
sourceFlatObjectMetadata: FlatObjectMetadata;
workspaceId: string;
workspaceCustomApplicationId: string;
flatApplication: FlatApplication;
};
export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({
existingFlatObjectMetadataMaps,
existingFlatFieldMetadataMaps,
sourceFlatObjectMetadata,
createFieldInput,
workspaceId,
workspaceCustomApplicationId,
flatApplication,
}: FromRelationCreateFieldInputToFlatFieldMetadataArgs): Promise<
FieldInputTranspilationResult<SourceTargetMorphOrRelationFlatFieldAndFlatIndex>
> => {
@@ -59,6 +64,13 @@ export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({
const { relationCreationPayload, targetFlatObjectMetadata } =
relationValidationResult.result;
const { junctionTargetFieldId } = extractJunctionTargetSettingsFromSettings(
createFieldInput.settings,
);
const junctionTargetFlatFieldMetadata = isDefined(junctionTargetFieldId)
? existingFlatFieldMetadataMaps.byId[junctionTargetFieldId]
: undefined;
const generateResult = generateMorphOrRelationFlatFieldMetadataPair({
createFieldInput: {
...createFieldInput,
@@ -72,7 +84,8 @@ export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({
targetFlatObjectMetadata,
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
workspaceId,
workspaceCustomApplicationId,
flatApplication,
junctionTargetFlatFieldMetadata,
});
return {
@@ -5,6 +5,7 @@ import {
isDefined,
} from 'twenty-shared/utils';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
@@ -23,7 +24,7 @@ import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-fiel
type FromUpdateFieldInputToFlatFieldMetadataArgs = {
updateFieldInput: UpdateFieldInput;
workspaceCustomApplicationId: string;
flatApplication: FlatApplication;
isSystemBuild: boolean;
} & Pick<
AllFlatEntityMaps,
@@ -41,7 +42,7 @@ type FlatFieldMetadataAndIndexToUpdate = {
flatFieldMetadatasToCreate: FlatFieldMetadata[];
} & FlatFieldMetadataUpdateSideEffects;
export const fromUpdateFieldInputToFlatFieldMetadata = ({
workspaceCustomApplicationId,
flatApplication,
flatIndexMaps,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatFieldMetadataMaps,
@@ -109,7 +110,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
FieldMetadataType.MORPH_RELATION,
)
? computeFlatFieldToUpdateFromMorphRelationUpdatePayload({
workspaceCustomApplicationId,
flatApplication,
morphRelationsUpdatePayload:
rawUpdateFieldInput?.morphRelationsUpdatePayload,
flatFieldMetadataMaps: flatFieldMetadataMaps,
@@ -149,7 +150,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
toFlatFieldMetadata,
flatViewMaps,
flatViewFieldMaps,
workspaceCustomApplicationId,
flatApplication,
});
if (sideEffectResult.status === 'fail') {
@@ -6,11 +6,11 @@ import {
} from 'twenty-shared/types';
import { v4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { extractJunctionTargetSettingsFromSettings } from 'src/engine/metadata-modules/flat-field-metadata/utils/extract-junction-target-settings-from-settings.util';
import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util';
import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
@@ -22,23 +22,39 @@ type ComputeFieldMetadataRelationSettingsForRelationTypeArgs = {
relationType: RelationType;
joinColumnName: string;
junctionTargetFieldId?: string;
junctionTargetFieldUniversalIdentifier?: string;
};
const computeFieldMetadataRelationSettingsForRelationType = ({
relationType,
joinColumnName,
junctionTargetFieldId,
}: ComputeFieldMetadataRelationSettingsForRelationTypeArgs) => {
junctionTargetFieldUniversalIdentifier,
}: ComputeFieldMetadataRelationSettingsForRelationTypeArgs): Pick<
FlatFieldMetadata<MorphOrRelationFieldMetadataType>,
'settings' | 'universalSettings'
> => {
if (relationType === RelationType.MANY_TO_ONE) {
return {
const settings = {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName,
};
return { settings, universalSettings: settings };
}
return {
relationType: RelationType.ONE_TO_MANY,
...(junctionTargetFieldId && { junctionTargetFieldId }),
settings: {
relationType: RelationType.ONE_TO_MANY,
...(junctionTargetFieldId && { junctionTargetFieldId }),
},
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
...(junctionTargetFieldUniversalIdentifier && {
junctionTargetFieldUniversalIdentifier,
}),
},
};
};
@@ -53,8 +69,9 @@ type GenerateMorphOrRelationFlatFieldMetadataPairArgs = {
> & { type: MorphOrRelationFieldMetadataType };
workspaceId: string;
morphId?: string | null;
workspaceCustomApplicationId: string;
flatApplication: FlatApplication;
targetFieldName?: string;
junctionTargetFlatFieldMetadata?: FlatFieldMetadata;
};
export type SourceTargetMorphOrRelationFlatFieldAndFlatIndex = {
@@ -68,27 +85,30 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
targetFlatObjectMetadata,
targetFlatFieldMetadataType,
workspaceId,
workspaceCustomApplicationId,
flatApplication,
sourceFlatObjectMetadataJoinColumnName,
morphId = null,
targetFieldName,
junctionTargetFlatFieldMetadata,
}: GenerateMorphOrRelationFlatFieldMetadataPairArgs): SourceTargetMorphOrRelationFlatFieldAndFlatIndex => {
const sourceFlatFieldMetadataType = createFieldInput.type;
const { relationCreationPayload } = createFieldInput;
const { junctionTargetFieldId } = extractJunctionTargetSettingsFromSettings(
createFieldInput.settings,
);
const sourceFlatFieldMetadataSettings =
computeFieldMetadataRelationSettingsForRelationType({
joinColumnName: sourceFlatObjectMetadataJoinColumnName,
relationType: relationCreationPayload.type,
junctionTargetFieldId,
});
const {
settings: sourceFlatFieldMetadataSettings,
universalSettings: sourceFlatFieldMetadataUniversalSettings,
} = computeFieldMetadataRelationSettingsForRelationType({
joinColumnName: sourceFlatObjectMetadataJoinColumnName,
relationType: relationCreationPayload.type,
junctionTargetFieldId: junctionTargetFlatFieldMetadata?.id,
junctionTargetFieldUniversalIdentifier:
junctionTargetFlatFieldMetadata?.universalIdentifier,
});
const targetRelationTargetFieldMetadataId = v4();
const sourceRelationTargetFieldMetadataId = v4();
const sourceFieldUniversalIdentifier = v4();
const targetFieldUniversalIdentifier = v4();
const defaultDescriptionFromField =
buildDescriptionForRelationFieldMetadataOnFromField({
@@ -105,10 +125,15 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
'flatRelationTargetFieldMetadata'
> = {
...getDefaultFlatFieldMetadata({
createFieldInput,
createFieldInput: {
...createFieldInput,
universalIdentifier: sourceFieldUniversalIdentifier,
},
workspaceId,
fieldMetadataId: sourceRelationTargetFieldMetadataId,
workspaceCustomApplicationId,
flatApplication,
objectMetadataUniversalIdentifier:
sourceFlatObjectMetadata.universalIdentifier,
}),
morphId:
sourceFlatFieldMetadataType === FieldMetadataType.MORPH_RELATION
@@ -121,9 +146,14 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
createFieldInput.description ?? defaultDescriptionFromField.description,
defaultValue: null,
settings: sourceFlatFieldMetadataSettings,
universalSettings: sourceFlatFieldMetadataUniversalSettings,
options: null,
relationTargetFieldMetadataId: targetRelationTargetFieldMetadataId,
relationTargetObjectMetadataId: targetFlatObjectMetadata.id,
relationTargetObjectMetadataUniversalIdentifier:
targetFlatObjectMetadata.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier:
targetFieldUniversalIdentifier,
};
const targetCreateFieldInput: CreateFieldInput = {
@@ -141,25 +171,32 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
workspaceId,
isSystem: createFieldInput.isSystem ?? false,
};
const targetFlatFieldMetadataSettings =
computeFieldMetadataRelationSettingsForRelationType({
joinColumnName: computeMorphOrRelationFieldJoinColumnName({
name: targetCreateFieldInput.name,
}),
relationType:
relationCreationPayload.type === RelationType.ONE_TO_MANY
? RelationType.MANY_TO_ONE
: RelationType.ONE_TO_MANY,
});
const {
settings: targetFlatFieldMetadataSettings,
universalSettings: targetFlatFieldMetadataUniversalSettings,
} = computeFieldMetadataRelationSettingsForRelationType({
joinColumnName: computeMorphOrRelationFieldJoinColumnName({
name: targetCreateFieldInput.name,
}),
relationType:
relationCreationPayload.type === RelationType.ONE_TO_MANY
? RelationType.MANY_TO_ONE
: RelationType.ONE_TO_MANY,
});
const targetFlatFieldMetadata: FlatFieldMetadata<
typeof targetFlatFieldMetadataType
> = {
...getDefaultFlatFieldMetadata({
createFieldInput: targetCreateFieldInput,
createFieldInput: {
...targetCreateFieldInput,
universalIdentifier: targetFieldUniversalIdentifier,
},
workspaceId,
fieldMetadataId: targetRelationTargetFieldMetadataId,
workspaceCustomApplicationId,
flatApplication,
objectMetadataUniversalIdentifier:
targetFlatObjectMetadata.universalIdentifier,
}),
morphId:
targetFlatFieldMetadataType === FieldMetadataType.MORPH_RELATION
@@ -168,9 +205,14 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
type: targetFlatFieldMetadataType,
defaultValue: null,
settings: targetFlatFieldMetadataSettings,
universalSettings: targetFlatFieldMetadataUniversalSettings,
options: null,
relationTargetFieldMetadataId: sourceRelationTargetFieldMetadataId,
relationTargetObjectMetadataId: sourceFlatObjectMetadata.id,
relationTargetObjectMetadataUniversalIdentifier:
sourceFlatObjectMetadata.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier:
sourceFieldUniversalIdentifier,
};
const indexMetadata: FlatIndexMetadata = generateIndexForFlatFieldMetadata({
@@ -1,6 +1,7 @@
import { extractAndSanitizeObjectStringFields } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
import { generateNullable } from 'src/engine/metadata-modules/field-metadata/utils/generate-nullable';
@@ -10,13 +11,15 @@ type GetDefaultFlatFieldMetadataArgs = {
fieldMetadataId: string;
createFieldInput: Omit<CreateFieldInput, 'workspaceId'>;
workspaceId: string;
workspaceCustomApplicationId: string;
flatApplication: FlatApplication;
objectMetadataUniversalIdentifier: string;
};
export const getDefaultFlatFieldMetadata = ({
createFieldInput,
fieldMetadataId,
workspaceId,
workspaceCustomApplicationId,
flatApplication,
objectMetadataUniversalIdentifier,
}: GetDefaultFlatFieldMetadataArgs) => {
const { defaultValue, settings } = extractAndSanitizeObjectStringFields(
createFieldInput,
@@ -58,9 +61,18 @@ export const getDefaultFlatFieldMetadata = ({
updatedAt: createdAt,
isUIReadOnly: createFieldInput.isUIReadOnly ?? false,
morphId: null,
applicationId:
createFieldInput.applicationId ?? workspaceCustomApplicationId,
applicationId: flatApplication.id,
viewFilterIds: [],
kanbanAggregateOperationViewIds: [],
applicationUniversalIdentifier: flatApplication.universalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
} as const satisfies FlatFieldMetadata;
};
@@ -1,6 +1,7 @@
import { type FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
@@ -41,7 +42,7 @@ type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo<
| 'flatViewMaps'
| 'flatViewFieldMaps'
> & {
workspaceCustomApplicationId: string;
flatApplication: FlatApplication;
};
export const FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS: FlatFieldMetadataUpdateSideEffects =
@@ -70,7 +71,7 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
flatViewGroupMaps,
flatViewMaps,
flatViewFieldMaps,
workspaceCustomApplicationId,
flatApplication,
}: HandleFlatFieldMetadataUpdateSideEffectArgs): FieldInputTranspilationResult<FlatFieldMetadataUpdateSideEffects> => {
const sideEffectResult = structuredClone(
FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS,
@@ -130,7 +131,7 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
flatIndexMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
workspaceCustomApplicationId,
flatApplication,
});
if (indexChangesSideEffectResult.status === 'fail') {
@@ -2,6 +2,7 @@ import { msg } from '@lingui/core/macro';
import { type FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
@@ -28,7 +29,7 @@ type FromToFlatFieldMetadataAndFlatEntityMaps = FromTo<
AllFlatEntityMaps,
'flatObjectMetadataMaps' | 'flatFieldMetadataMaps' | 'flatIndexMaps'
> & {
workspaceCustomApplicationId: string;
flatApplication: FlatApplication;
};
const FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT: FieldMetadataUpdateIndexSideEffect =
{
@@ -43,7 +44,7 @@ export const handleIndexChangesDuringFieldUpdate = ({
flatIndexMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
workspaceCustomApplicationId,
flatApplication,
}: FromToFlatFieldMetadataAndFlatEntityMaps): FieldInputTranspilationResult<FieldMetadataUpdateIndexSideEffect> => {
if (
!hasIndexRelevantChanges({
@@ -81,7 +82,7 @@ export const handleIndexChangesDuringFieldUpdate = ({
relatedIndexes,
flatObjectMetadata,
flatFieldMetadataMaps,
workspaceCustomApplicationId,
flatApplication,
});
};
@@ -127,13 +128,13 @@ const handleExistingIndexes = ({
relatedIndexes,
flatObjectMetadata,
flatFieldMetadataMaps,
workspaceCustomApplicationId,
flatApplication,
}: {
relatedIndexes: FlatIndexMetadata[];
flatObjectMetadata: FlatObjectMetadata;
flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps'];
} & FromTo<FlatFieldMetadata, 'flatFieldMetadata'> & {
workspaceCustomApplicationId: string;
flatApplication: FlatApplication;
}): FieldInputTranspilationResult<FieldMetadataUpdateIndexSideEffect> => {
if (
toFlatFieldMetadata.isUnique === false &&
@@ -155,7 +156,7 @@ const handleExistingIndexes = ({
if (
isDefined(uniqueIndexToDelete) &&
((isDefined(uniqueIndexToDelete.applicationId) &&
uniqueIndexToDelete.applicationId !== workspaceCustomApplicationId) ||
uniqueIndexToDelete.applicationId !== flatApplication.id) ||
!uniqueIndexToDelete.isCustom)
) {
return {
@@ -10,6 +10,11 @@ export const getFlatObjectMetadataMock = (
overrides: FlatObjectMetadataOverrides,
): FlatObjectMetadata => {
const createdAt = '2024-01-01T00:00:00.000Z';
const applicationId = overrides.applicationId ?? faker.string.uuid();
const labelIdentifierFieldMetadataId =
overrides.labelIdentifierFieldMetadataId ?? faker.string.uuid();
const imageIdentifierFieldMetadataId =
overrides.imageIdentifierFieldMetadataId ?? faker.string.uuid();
return {
viewIds: [],
@@ -18,7 +23,7 @@ export const getFlatObjectMetadataMock = (
description: 'default flat object metadata description',
icon: 'icon',
id: faker.string.uuid(),
imageIdentifierFieldMetadataId: faker.string.uuid(),
imageIdentifierFieldMetadataId,
isActive: true,
isAuditLogged: true,
isCustom: true,
@@ -27,20 +32,28 @@ export const getFlatObjectMetadataMock = (
isSearchable: true,
isSystem: false,
isUIReadOnly: false,
labelIdentifierFieldMetadataId: faker.string.uuid(),
labelIdentifierFieldMetadataId,
labelPlural: 'default flat object metadata label plural',
labelSingular: 'default flat object metadata label singular',
namePlural: 'defaultflatObjectMetadataNamePlural',
nameSingular: 'defaultflatObjectMetadataNameSingular',
shortcut: 'shortcut',
standardId: null,
applicationId: faker.string.uuid(),
applicationId,
standardOverrides: null,
targetTableName: '',
workspaceId: faker.string.uuid(),
createdAt,
updatedAt: createdAt,
duplicateCriteria: null,
applicationUniversalIdentifier: applicationId,
fieldUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier:
labelIdentifierFieldMetadataId,
imageIdentifierFieldMetadataUniversalIdentifier:
imageIdentifierFieldMetadataId,
...overrides,
};
};
@@ -1,13 +1,12 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityFromV2 } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from-v2.type';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
type BaseFlatObjectMetadata = FlatEntityFrom<
type BaseFlatObjectMetadata = FlatEntityFromV2<
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;
};
export type FlatObjectMetadata = BaseFlatObjectMetadata & {
// NOTE: below fields are not reflected on the final UniversalFlatEntity either they should we should define a common source
// TODO remove once https://github.com/twentyhq/core-team-issues/issues/2172 has been resolved
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
imageIdentifierFieldMetadataUniversalIdentifier: string | null;
};
@@ -6,6 +6,7 @@ import { v4 } from 'uuid';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
@@ -19,14 +20,14 @@ type FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs
{
createObjectInput: CreateObjectInput;
workspaceId: string;
workspaceCustomApplicationId: string;
flatApplication: FlatApplication;
existingFeatureFlagsMap: FeatureFlagMap;
} & Pick<AllFlatEntityMaps, 'flatObjectMetadataMaps'>;
export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate =
({
createObjectInput: rawCreateObjectInput,
workspaceId,
workspaceCustomApplicationId,
flatApplication,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
existingFeatureFlagsMap,
}: FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs): {
@@ -50,11 +51,14 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
);
const objectMetadataId = v4();
const universalIdentifier = createObjectInput.universalIdentifier ?? v4();
const defaultFlatFieldForCustomObjectMaps =
buildDefaultFlatFieldMetadatasForCustomObject({
flatObjectMetadata: {
id: objectMetadataId,
applicationId: workspaceCustomApplicationId,
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
universalIdentifier,
},
workspaceId,
skipNameField: createObjectInput.skipNameField,
@@ -62,11 +66,12 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
const createdAt = new Date().toISOString();
// Use nameField.id if it exists, otherwise use idField.id (for junction tables without name)
const nameField = defaultFlatFieldForCustomObjectMaps.fields.nameField as
| FlatFieldMetadata
| undefined;
const nameField = defaultFlatFieldForCustomObjectMaps.fields.nameField;
const labelIdentifierFieldMetadataId =
nameField?.id ?? defaultFlatFieldForCustomObjectMaps.fields.idField.id;
const labelIdentifierFieldMetadataUniversalIdentifier =
nameField?.universalIdentifier ??
defaultFlatFieldForCustomObjectMaps.fields.idField.universalIdentifier;
const flatObjectMetadataToCreate: FlatObjectMetadata = {
fieldIds: [],
@@ -95,10 +100,16 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
shortcut: createObjectInput.shortcut ?? null,
standardId: createObjectInput.standardId ?? null,
standardOverrides: null,
applicationId: workspaceCustomApplicationId,
universalIdentifier: createObjectInput.universalIdentifier ?? v4(),
applicationId: flatApplication.id,
universalIdentifier,
targetTableName: 'DEPRECATED',
workspaceId,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
fieldUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier,
imageIdentifierFieldMetadataUniversalIdentifier: null,
};
const {
@@ -108,7 +119,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
existingFlatObjectMetadataMaps,
sourceFlatObjectMetadata: flatObjectMetadataToCreate,
workspaceId,
workspaceCustomApplicationId,
flatApplication,
existingFeatureFlagsMap,
});
@@ -76,24 +76,17 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({
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,
},
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
imageIdentifierFieldMetadataUniversalIdentifier,
fieldUniversalIdentifiers: objectMetadataEntity.fields.map(
({ universalIdentifier }) => universalIdentifier,
),
indexMetadataUniversalIdentifiers: objectMetadataEntity.indexMetadatas.map(
({ universalIdentifier }) => universalIdentifier,
),
viewUniversalIdentifiers: objectMetadataEntity.views.map(
({ universalIdentifier }) => universalIdentifier,
),
};
};
@@ -8,7 +8,7 @@ type PageLayoutWidgetConfiguration = PageLayoutWidgetEntity['configuration'];
type UniversalPageLayoutWidgetConfiguration = NonNullable<
FlatPageLayoutWidget['__universal']
>['configuration'];
>['universalConfiguration'];
// Field metadata IDs in widget configurations don't need to reference existing entities
const getFieldMetadataUniversalIdentifier = ({
@@ -85,10 +85,7 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({
applicationUniversalIdentifier,
pageLayoutTabUniversalIdentifier,
objectMetadataUniversalIdentifier,
gridPosition: pageLayoutWidgetEntityWithoutRelations.gridPosition,
configuration: configurationWithUniversalIdentifiers,
conditionalDisplay:
pageLayoutWidgetEntityWithoutRelations.conditionalDisplay,
universalConfiguration: configurationWithUniversalIdentifiers,
},
};
};
@@ -129,7 +129,6 @@ export const fromRowLevelPermissionPredicateEntityToFlatRowLevelPermissionPredic
roleUniversalIdentifier,
workspaceMemberFieldMetadataUniversalIdentifier,
rowLevelPermissionPredicateGroupUniversalIdentifier,
value: rowLevelPermissionPredicateEntityWithoutRelations.value,
},
};
};
@@ -81,7 +81,6 @@ export const fromViewFilterEntityToFlatViewFilter = ({
fieldMetadataUniversalIdentifier,
viewFilterGroupUniversalIdentifier,
viewUniversalIdentifier,
value: viewFilterEntityWithoutRelations.value,
},
};
};
@@ -37,12 +37,6 @@ export const fromLogicFunctionEntityToFlatLogicFunction = ({
__universal: {
universalIdentifier: logicFunctionEntity.universalIdentifier,
applicationUniversalIdentifier,
publishedVersions: logicFunctionEntity.publishedVersions,
toolInputSchema: logicFunctionEntity.toolInputSchema,
cronTriggerSettings: logicFunctionEntity.cronTriggerSettings,
databaseEventTriggerSettings:
logicFunctionEntity.databaseEventTriggerSettings,
httpRouteTriggerSettings: logicFunctionEntity.httpRouteTriggerSettings,
},
};
};
@@ -10,6 +10,7 @@ export enum ObjectMetadataExceptionCode {
INVALID_OBJECT_INPUT = 'INVALID_OBJECT_INPUT',
OBJECT_MUTATION_NOT_ALLOWED = 'OBJECT_MUTATION_NOT_ALLOWED',
OBJECT_ALREADY_EXISTS = 'OBJECT_ALREADY_EXISTS',
APPLICATION_NOT_FOUND = 'APPLICATION_NOT_FOUND',
MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD = 'MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD',
INVALID_ORM_OUTPUT = 'INVALID_ORM_OUTPUT',
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
@@ -28,6 +29,8 @@ const getObjectMetadataExceptionUserFriendlyMessage = (
return msg`This object cannot be modified.`;
case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS:
return msg`An object with this name already exists.`;
case ObjectMetadataExceptionCode.APPLICATION_NOT_FOUND:
return msg`Application not found.`;
case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
return msg`Custom object is missing a label identifier field.`;
case ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT:
@@ -331,11 +331,24 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
const {
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
featureFlagsMap: existingFeatureFlagsMap,
flatApplicationMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'featureFlagsMap',
'flatApplicationMaps',
]);
const ownerFlatApplication = isDefined(applicationId)
? flatApplicationMaps.byId[applicationId]
: workspaceCustomFlatApplication;
if (!isDefined(ownerFlatApplication)) {
throw new ObjectMetadataException(
`Could not find related application ${applicationId}`,
ObjectMetadataExceptionCode.APPLICATION_NOT_FOUND,
);
}
const {
flatObjectMetadataToCreate,
flatIndexMetadataToCreate,
@@ -344,8 +357,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
} = fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate({
createObjectInput,
workspaceId,
workspaceCustomApplicationId:
applicationId ?? workspaceCustomFlatApplication.id,
flatApplication: ownerFlatApplication,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
existingFeatureFlagsMap,
});
@@ -15,7 +15,13 @@ import {
type BuildDefaultFlatFieldMetadataForCustomObjectArgs = {
workspaceId: string;
flatObjectMetadata: NonNullableRequired<
Pick<FlatObjectMetadata, 'id' | 'applicationId'>
Pick<
FlatObjectMetadata,
| 'id'
| 'applicationId'
| 'universalIdentifier'
| 'applicationUniversalIdentifier'
>
>;
skipNameField?: boolean;
};
@@ -26,7 +32,12 @@ export type DefaultFlatFieldForCustomObjectMaps = ReturnType<
// This could be replaced totally by an import schema + its transpilation when it's ready
export const buildDefaultFlatFieldMetadatasForCustomObject = ({
workspaceId,
flatObjectMetadata: { id: objectMetadataId, applicationId },
flatObjectMetadata: {
id: objectMetadataId,
applicationId,
applicationUniversalIdentifier,
universalIdentifier: objectMetadataUniversalIdentifier,
},
skipNameField = false,
}: BuildDefaultFlatFieldMetadataForCustomObjectArgs) => {
const createdAt = new Date().toISOString();
@@ -65,6 +76,16 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
settings: null,
morphId: null,
applicationId,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
const nameFieldId = skipNameField ? null : v4();
@@ -105,6 +126,16 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
settings: null,
morphId: null,
applicationId,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
const createdAtFieldId = v4();
@@ -142,6 +173,16 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
settings: null,
morphId: null,
applicationId,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
const updatedAtFieldId = v4();
@@ -179,6 +220,16 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
settings: null,
morphId: null,
applicationId,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
const deletedAtFieldId = v4();
@@ -216,6 +267,16 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
settings: null,
morphId: null,
applicationId,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
const createdByFieldId = v4();
@@ -252,6 +313,16 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
settings: null,
morphId: null,
applicationId,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
const updatedByFieldId = v4();
@@ -288,6 +359,16 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
settings: null,
morphId: null,
applicationId,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
const positionFieldId = v4();
@@ -325,6 +406,16 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
settings: null,
morphId: null,
applicationId,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
const searchVectorFieldId = v4();
@@ -367,6 +458,16 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
},
morphId: null,
applicationId,
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
return {
@@ -5,6 +5,7 @@ import { capitalize, isDefined } from 'twenty-shared/utils';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
@@ -53,7 +54,7 @@ export type BuildDefaultRelationFieldsForCustomObjectArgs = {
existingFlatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
workspaceId: string;
sourceFlatObjectMetadata: FlatObjectMetadata;
workspaceCustomApplicationId: string;
flatApplication: FlatApplication;
};
type SourceAndTargetFlatFieldMetadatasRecord = {
@@ -71,7 +72,7 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
existingFlatObjectMetadataMaps,
sourceFlatObjectMetadata,
workspaceId,
workspaceCustomApplicationId,
flatApplication,
}: BuildDefaultRelationFieldsForCustomObjectArgs): SourceAndTargetFlatFieldMetadatasRecord => {
const objectIdByNameSingular = Object.values(
existingFlatObjectMetadataMaps.byId,
@@ -156,7 +157,7 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
? FieldMetadataType.MORPH_RELATION
: FieldMetadataType.RELATION,
workspaceId,
workspaceCustomApplicationId,
flatApplication,
sourceFlatObjectMetadataJoinColumnName: joinColumnName,
morphId,
targetFieldName: fieldName,
@@ -43,6 +43,7 @@ export const objectMetadataGraphqlApiExceptionHandler = (
case ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT:
throw new InternalServerError(error);
case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
case ObjectMetadataExceptionCode.APPLICATION_NOT_FOUND:
throw error;
default: {
return assertUnreachable(error.code);
@@ -123,6 +123,12 @@ describe('WorkspaceEntityManager', () => {
duplicateCriteria: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
applicationUniversalIdentifier: 'test-application-id',
fieldUniversalIdentifiers: ['field-id'],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
imageIdentifierFieldMetadataUniversalIdentifier: null,
};
(getObjectMetadataFromEntityTarget as jest.Mock).mockReturnValue(
@@ -162,6 +168,16 @@ describe('WorkspaceEntityManager', () => {
relationTargetObjectMetadataId: null,
morphId: null,
applicationId: 'application-id',
applicationUniversalIdentifier: 'application-id',
objectMetadataUniversalIdentifier: 'test-entity-id',
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
const flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata> = {
@@ -97,6 +97,16 @@ describe('WorkspaceRepository', () => {
kanbanAggregateOperationViewIds: [],
viewFieldIds: [],
mainGroupByFieldMetadataViewIds: [],
applicationUniversalIdentifier: 'application-id',
objectMetadataUniversalIdentifier: 'test-metadata-id',
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
mockInternalContext = {
@@ -8,39 +8,44 @@ import { getColumnNameToFieldMetadataIdMap } from 'src/engine/twenty-orm/utils/g
describe('getColumnNameToFieldMetadataIdMap', () => {
const createMockFlatObjectMetadata = (
fieldIds: string[],
): FlatObjectMetadata =>
({
id: 'test-object-id',
nameSingular: 'test',
namePlural: 'tests',
labelSingular: 'Test',
labelPlural: 'Tests',
icon: 'IconTest',
targetTableName: 'test',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
isAuditLogged: false,
isSearchable: false,
workspaceId: 'test-workspace-id',
universalIdentifier: 'test-object-id',
indexMetadataIds: [],
fieldIds,
viewIds: [],
applicationId: 'test-application-id',
isLabelSyncedWithName: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
shortcut: null,
description: null,
standardOverrides: null,
isUIReadOnly: false,
standardId: null,
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
duplicateCriteria: null,
}) as FlatObjectMetadata;
): FlatObjectMetadata => ({
id: 'test-object-id',
nameSingular: 'test',
namePlural: 'tests',
labelSingular: 'Test',
labelPlural: 'Tests',
icon: 'IconTest',
targetTableName: 'test',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
isAuditLogged: false,
isSearchable: false,
workspaceId: 'test-workspace-id',
universalIdentifier: 'test-object-id',
indexMetadataIds: [],
fieldIds,
viewIds: [],
applicationId: 'test-application-id',
isLabelSyncedWithName: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
shortcut: null,
description: null,
standardOverrides: null,
isUIReadOnly: false,
standardId: null,
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
duplicateCriteria: null,
applicationUniversalIdentifier: 'test-application-id',
fieldUniversalIdentifiers: fieldIds,
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
imageIdentifierFieldMetadataUniversalIdentifier: null,
});
const createMockFlatFieldMetadata = (
id: string,
@@ -8,39 +8,44 @@ import { getFieldMetadataIdToColumnNamesMap } from 'src/engine/twenty-orm/utils/
describe('getFieldMetadataIdToColumnNamesMap', () => {
const createMockFlatObjectMetadata = (
fieldIds: string[],
): FlatObjectMetadata =>
({
id: 'test-object-id',
nameSingular: 'test',
namePlural: 'tests',
labelSingular: 'Test',
labelPlural: 'Tests',
icon: 'IconTest',
targetTableName: 'test',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
isAuditLogged: false,
isSearchable: false,
workspaceId: 'test-workspace-id',
universalIdentifier: 'test-object-id',
indexMetadataIds: [],
fieldIds,
viewIds: [],
applicationId: 'test-application-id',
isLabelSyncedWithName: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
shortcut: null,
description: null,
standardOverrides: null,
isUIReadOnly: false,
standardId: null,
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
duplicateCriteria: null,
}) as FlatObjectMetadata;
): FlatObjectMetadata => ({
id: 'test-object-id',
nameSingular: 'test',
namePlural: 'tests',
labelSingular: 'Test',
labelPlural: 'Tests',
icon: 'IconTest',
targetTableName: 'test',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
isAuditLogged: false,
isSearchable: false,
workspaceId: 'test-workspace-id',
universalIdentifier: 'test-object-id',
indexMetadataIds: [],
fieldIds,
viewIds: [],
applicationId: 'test-application-id',
isLabelSyncedWithName: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
shortcut: null,
description: null,
standardOverrides: null,
isUIReadOnly: false,
standardId: null,
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
duplicateCriteria: null,
applicationUniversalIdentifier: 'test-application-id',
fieldUniversalIdentifiers: fieldIds,
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
imageIdentifierFieldMetadataUniversalIdentifier: null,
});
const createMockFlatFieldMetadata = (
id: string,
@@ -8,39 +8,44 @@ import { isRecordMatchingRLSRowLevelPermissionPredicate } from 'src/engine/twent
describe('isRecordMatchingRLSRowLevelPermissionPredicate', () => {
const createMockFlatObjectMetadata = (
fieldIds: string[],
): FlatObjectMetadata =>
({
id: 'test-object-id',
nameSingular: 'test',
namePlural: 'tests',
labelSingular: 'Test',
labelPlural: 'Tests',
icon: 'IconTest',
targetTableName: 'test',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
isAuditLogged: false,
isSearchable: false,
workspaceId: 'test-workspace-id',
universalIdentifier: 'test-object-id',
indexMetadataIds: [],
fieldIds,
viewIds: [],
applicationId: 'test-application-id',
isLabelSyncedWithName: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
shortcut: null,
description: null,
standardOverrides: null,
isUIReadOnly: false,
standardId: null,
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
duplicateCriteria: null,
}) as FlatObjectMetadata;
): FlatObjectMetadata => ({
id: 'test-object-id',
nameSingular: 'test',
namePlural: 'tests',
labelSingular: 'Test',
labelPlural: 'Tests',
icon: 'IconTest',
targetTableName: 'test',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
isAuditLogged: false,
isSearchable: false,
workspaceId: 'test-workspace-id',
universalIdentifier: 'test-object-id',
indexMetadataIds: [],
fieldIds,
viewIds: [],
applicationId: 'test-application-id',
isLabelSyncedWithName: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
shortcut: null,
description: null,
standardOverrides: null,
isUIReadOnly: false,
standardId: null,
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
duplicateCriteria: null,
applicationUniversalIdentifier: 'test-application-id',
fieldUniversalIdentifiers: fieldIds,
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
imageIdentifierFieldMetadataUniversalIdentifier: null,
});
const createMockFlatFieldMetadata = (
id: string,
@@ -100,5 +100,16 @@ export const createStandardFieldFlatMetadata = <
mainGroupByFieldMetadataViewIds: [],
createdAt: now,
updatedAt: now,
applicationUniversalIdentifier: twentyStandardApplicationId,
objectMetadataUniversalIdentifier:
STANDARD_OBJECTS[objectName].universalIdentifier,
relationTargetObjectMetadataUniversalIdentifier: null,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
};
@@ -83,6 +83,10 @@ export const createStandardRelationFieldFlatMetadata = <
const targetFieldIds =
standardObjectMetadataRelatedEntityIds[targetObjectName].fields;
const targetObjectFields = STANDARD_OBJECTS[targetObjectName].fields;
const targetFieldDefinition =
targetObjectFields[targetFieldName as keyof typeof targetObjectFields];
return {
id: fieldIds[fieldName as keyof typeof fieldIds].id,
universalIdentifier: fieldDefinition.universalIdentifier,
@@ -117,5 +121,18 @@ export const createStandardRelationFieldFlatMetadata = <
mainGroupByFieldMetadataViewIds: [],
createdAt: now,
updatedAt: now,
applicationUniversalIdentifier: twentyStandardApplicationId,
objectMetadataUniversalIdentifier:
STANDARD_OBJECTS[objectName].universalIdentifier,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECTS[targetObjectName].universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier:
targetFieldDefinition.universalIdentifier,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
universalSettings: null,
};
};
@@ -1,4 +1,5 @@
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
import { type AllStandardObjectName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-name.type';
import { type StandardBuilderArgs } from 'src/engine/workspace-manager/twenty-standard-application/types/metadata-standard-buillder-args.type';
@@ -52,42 +53,56 @@ export const createStandardObjectFlatMetadata = <
standardObjectMetadataRelatedEntityIds,
twentyStandardApplicationId,
now,
}: CreateStandardObjectArgs<O>): FlatObjectMetadata => ({
universalIdentifier,
standardId: universalIdentifier,
applicationId: twentyStandardApplicationId,
workspaceId,
nameSingular,
namePlural,
labelSingular,
labelPlural,
description,
icon,
isCustom: false,
isRemote: false,
isActive: true,
isSystem,
isSearchable,
isAuditLogged,
isUIReadOnly,
isLabelSyncedWithName: false,
standardOverrides: null,
duplicateCriteria,
shortcut,
labelIdentifierFieldMetadataId:
standardObjectMetadataRelatedEntityIds[nameSingular].fields[
}: CreateStandardObjectArgs<O>): FlatObjectMetadata => {
const labelIdentifierFieldMetadataUniversalIdentifier =
// @ts-expect-error ignore
STANDARD_OBJECTS[nameSingular as keyof typeof STANDARD_OBJECTS].fields[
labelIdentifierFieldMetadataName
].id,
imageIdentifierFieldMetadataId: imageIdentifierFieldMetadataName
? standardObjectMetadataRelatedEntityIds[nameSingular].fields[
imageIdentifierFieldMetadataName
].id
: null,
targetTableName: 'DEPRECATED',
fieldIds: [],
indexMetadataIds: [],
viewIds: [],
createdAt: now,
updatedAt: now,
id: standardObjectMetadataRelatedEntityIds[nameSingular].id,
});
].universalIdentifier;
return {
universalIdentifier,
standardId: universalIdentifier,
applicationId: twentyStandardApplicationId,
workspaceId,
nameSingular,
namePlural,
labelSingular,
labelPlural,
description,
icon,
isCustom: false,
isRemote: false,
isActive: true,
isSystem,
isSearchable,
isAuditLogged,
isUIReadOnly,
isLabelSyncedWithName: false,
standardOverrides: null,
duplicateCriteria,
shortcut,
labelIdentifierFieldMetadataId:
standardObjectMetadataRelatedEntityIds[nameSingular].fields[
labelIdentifierFieldMetadataName
].id,
imageIdentifierFieldMetadataId: imageIdentifierFieldMetadataName
? standardObjectMetadataRelatedEntityIds[nameSingular].fields[
imageIdentifierFieldMetadataName
].id
: null,
targetTableName: 'DEPRECATED',
fieldIds: [],
indexMetadataIds: [],
viewIds: [],
createdAt: now,
updatedAt: now,
id: standardObjectMetadataRelatedEntityIds[nameSingular].id,
applicationUniversalIdentifier: twentyStandardApplicationId,
fieldUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier,
imageIdentifierFieldMetadataUniversalIdentifier: null,
};
};
@@ -1,57 +0,0 @@
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',
conditionalDisplay: 'conditionalDisplay',
},
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];
@@ -0,0 +1,42 @@
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_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
fieldMetadata: {
settings: 'settings',
},
objectMetadata: {},
view: {},
viewField: {},
viewGroup: {},
viewFilter: {},
viewFilterGroup: {},
index: {},
role: {},
roleTarget: {},
rowLevelPermissionPredicate: {},
rowLevelPermissionPredicateGroup: {},
logicFunction: {},
webhook: {},
agent: {},
skill: {},
pageLayout: {},
pageLayoutTab: {},
pageLayoutWidget: {
configuration: 'configuration',
},
commandMenuItem: {},
navigationMenuItem: {},
frontComponent: {},
} as const satisfies {
[P in AllMetadataName]: Partial<{
[K in ExtractJsonbProperties<MetadataEntity<P>>]: K;
}>;
};
export type AllJsonbPropertiesWithSerializedPropertiesForMetadataName<
T extends AllMetadataName,
> =
keyof (typeof ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME)[T];
@@ -0,0 +1,146 @@
import { type Equal, type Expect } from 'twenty-shared/testing';
import { type SerializedRelation } from 'twenty-shared/types';
import { type ContainsSerializedRelation } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/contains-serialized-relation.type';
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
type EmptyObject = {};
// ContainsSerializedRelation checks for SerializedRelation in object properties
// It recurses into nested objects and arrays, but stops at primitives
// Direct property tests
// eslint-disable-next-line unused-imports/no-unused-vars
type DirectPropertyAssertions = [
// Direct SerializedRelation property
Expect<
Equal<ContainsSerializedRelation<{ targetId: SerializedRelation }>, true>
>,
// Nullable SerializedRelation property
Expect<
Equal<
ContainsSerializedRelation<{ targetId: SerializedRelation | null }>,
true
>
>,
// Optional SerializedRelation property
Expect<
Equal<ContainsSerializedRelation<{ targetId?: SerializedRelation }>, true>
>,
// Array of SerializedRelation
Expect<
Equal<ContainsSerializedRelation<{ ids: SerializedRelation[] }>, true>
>,
// Plain properties only - no SerializedRelation
Expect<
Equal<ContainsSerializedRelation<{ name: string; count: number }>, false>
>,
// Empty object
Expect<Equal<ContainsSerializedRelation<EmptyObject>, false>>,
];
// Nested object tests - should recurse
// eslint-disable-next-line unused-imports/no-unused-vars
type NestedObjectAssertions = [
// Nested object with SerializedRelation
Expect<
Equal<
ContainsSerializedRelation<{
nested: { targetId: SerializedRelation };
}>,
true
>
>,
// Deeply nested SerializedRelation
Expect<
Equal<
ContainsSerializedRelation<{
level1: { level2: { level3: { id: SerializedRelation } } };
}>,
true
>
>,
// Nested object without SerializedRelation
Expect<
Equal<
ContainsSerializedRelation<{
nested: { name: string; count: number };
}>,
false
>
>,
];
// Nested array tests - should recurse
// eslint-disable-next-line unused-imports/no-unused-vars
type NestedArrayAssertions = [
// Array of objects with SerializedRelation
Expect<
Equal<
ContainsSerializedRelation<{
items: { targetId: SerializedRelation }[];
}>,
true
>
>,
// 2D array of SerializedRelation
Expect<
Equal<ContainsSerializedRelation<{ matrix: SerializedRelation[][] }>, true>
>,
// Array of plain objects
Expect<
Equal<
ContainsSerializedRelation<{
items: { name: string }[];
}>,
false
>
>,
// 2D array of strings
Expect<Equal<ContainsSerializedRelation<{ matrix: string[][] }>, false>>,
];
// Primitive types - should return false (not objects)
// eslint-disable-next-line unused-imports/no-unused-vars
type PrimitiveAssertions = [
Expect<Equal<ContainsSerializedRelation<string>, false>>,
Expect<Equal<ContainsSerializedRelation<number>, false>>,
Expect<Equal<ContainsSerializedRelation<boolean>, false>>,
Expect<Equal<ContainsSerializedRelation<null>, false>>,
Expect<Equal<ContainsSerializedRelation<undefined>, false>>,
];
// Real-world tests
// eslint-disable-next-line unused-imports/no-unused-vars
type RealWorldAssertions = [
// Settings with SerializedRelation
Expect<
Equal<
ContainsSerializedRelation<{
relationType?: string;
junctionTargetFieldId?: SerializedRelation;
}>,
true
>
>,
// Workflow config with nested SerializedRelation in array
Expect<
Equal<
ContainsSerializedRelation<{
steps: { assigneeId: SerializedRelation; action: string }[];
}>,
true
>
>,
];
@@ -4,10 +4,8 @@ import {
type HasAllProperties,
} from 'twenty-shared/testing';
import {
type FieldMetadataDefaultOption,
type FieldMetadataType,
type FieldNumberVariant,
type LinkMetadata,
type NullablePartial,
type NumberDataType,
type RelationOnDeleteAction,
@@ -91,8 +89,9 @@ type UniversalFlatTransformationAssertions = [
>,
];
// JSONB properties are now prefixed with 'universal' in UniversalFlatFieldMetadata
type NarrowedTestCase =
UniversalFlatFieldMetadata<FieldMetadataType.RELATION>['settings'];
UniversalFlatFieldMetadata<FieldMetadataType.RELATION>['universalSettings'];
type NarrowedExpectedResult = {
relationType: RelationType;
@@ -107,7 +106,7 @@ type NarrowedExpectedResult = {
type SettingsTestCase = UniversalFlatFieldMetadata<
FieldMetadataType.RELATION | FieldMetadataType.NUMBER | FieldMetadataType.TEXT
>['settings'];
>['universalSettings'];
type SettingsExpectedResult =
| {
@@ -132,39 +131,8 @@ type SettingsExpectedResult =
}
| null;
type DefaultValueTestCase = UniversalFlatFieldMetadata<
| FieldMetadataType.RELATION
| FieldMetadataType.NUMBER
| FieldMetadataType.TEXT
| FieldMetadataType.LINKS
| FieldMetadataType.CURRENCY
>['defaultValue'];
type DefaultValueExpectedResult =
| string
| number
| null
| {
amountMicros: string | null;
currencyCode: string | null;
__JsonbPropertyBrand__?: undefined;
}
| {
primaryLinkLabel: string | null;
primaryLinkUrl: string | null;
secondaryLinks: LinkMetadata[] | null;
__JsonbPropertyBrand__?: undefined;
};
type OptionsTestCase =
UniversalFlatFieldMetadata<FieldMetadataType.RATING>['options'];
type OptionsExpectedResult = FieldMetadataDefaultOption[];
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
Expect<Equal<SettingsTestCase, SettingsExpectedResult>>,
Expect<Equal<NarrowedTestCase, NarrowedExpectedResult>>,
Expect<Equal<DefaultValueTestCase, DefaultValueExpectedResult>>,
Expect<Equal<OptionsTestCase, OptionsExpectedResult>>,
];
@@ -0,0 +1,24 @@
import {
type IsEmptyObject,
type IsNever,
type IsSerializedRelation,
} from 'twenty-shared/types';
type ContainsSerializedRelationInner<T> = T extends unknown
? IsNever<T> extends true
? false
: unknown extends T
? false
: IsSerializedRelation<T> extends true
? true
: T extends readonly (infer U)[]
? ContainsSerializedRelationInner<U>
: T extends object
? IsEmptyObject<T> extends true
? false
: ContainsSerializedRelationInner<T[keyof T]>
: false
: never;
export type ContainsSerializedRelation<T> =
true extends ContainsSerializedRelationInner<T> ? true : false;
@@ -1,12 +1,4 @@
import { type JSONB_PROPERTY_BRAND } from './jsonb-property.type';
export type HasJsonbPropertyBrand<T> =
typeof JSONB_PROPERTY_BRAND extends keyof T ? true : false;
// Distributive check: returns `true` if any member of a union has the brand
type HasJsonbBrandInUnion<T> = T extends unknown
? HasJsonbPropertyBrand<T>
: never;
import { type HasJsonbBrandInUnion } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/has-jsonb-brand-in-union.type';
export type ExtractJsonbProperties<T> = NonNullable<
{
@@ -0,0 +1,5 @@
import { type HasJsonbPropertyBrand } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/has-jsonb-brand.type';
export type HasJsonbBrandInUnion<T> = T extends unknown
? HasJsonbPropertyBrand<T>
: never;
@@ -0,0 +1,4 @@
import { type JSONB_PROPERTY_BRAND } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
export type HasJsonbPropertyBrand<T> =
typeof JSONB_PROPERTY_BRAND extends keyof T ? true : false;
@@ -7,7 +7,8 @@ import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-m
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 AllJsonbPropertiesForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-by-metadata-name.constant';
import { type AllJsonbPropertiesWithSerializedPropertiesForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant';
import { type ContainsSerializedRelation } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/contains-serialized-relation.type';
import { type FormatRecordSerializedRelationProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type';
export type UniversalSyncableFlatEntity = Omit<
@@ -19,6 +20,7 @@ export type UniversalSyncableFlatEntity = Omit<
export type UniversalFlatEntityExtraProperties<
TEntity extends SyncableEntity,
// Required to be passed for narrowed type
TMetadataName extends
AllMetadataName = FromMetadataEntityToMetadataName<TEntity>,
> = AddSuffixToEntityOneToManyProperties<TEntity, 'universalIdentifiers'> &
@@ -29,8 +31,13 @@ export type UniversalFlatEntityExtraProperties<
> & {
applicationUniversalIdentifier: string;
} & {
[P in AllJsonbPropertiesForMetadataName<TMetadataName> &
keyof TEntity]: FormatRecordSerializedRelationProperties<TEntity[P]>;
[P in AllJsonbPropertiesWithSerializedPropertiesForMetadataName<TMetadataName> &
keyof TEntity &
string as `universal${Capitalize<P>}`]: true extends ContainsSerializedRelation<
NonNullable<TEntity[P]>
>
? FormatRecordSerializedRelationProperties<TEntity[P]>
: null;
};
export type UniversalFlatEntityFrom<
@@ -46,7 +53,7 @@ export type UniversalFlatEntityFrom<
| ExtractEntityRelatedEntityProperties<TEntity>
| Extract<MetadataManyToOneJoinColumn<TMetadataName>, keyof TEntity>
| keyof CastRecordTypeOrmDatePropertiesToString<TEntity>
| AllJsonbPropertiesForMetadataName<TMetadataName>
| AllJsonbPropertiesWithSerializedPropertiesForMetadataName<TMetadataName>
> &
CastRecordTypeOrmDatePropertiesToString<TEntity> &
UniversalFlatEntityExtraProperties<TEntity, TMetadataName>;
@@ -1,13 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a created entity 1`] = `
{
"createdFlatEntityMaps": {
"byId": {
"field-id-1": {
"__universal": {},
"applicationId": "application-id-1",
"applicationUniversalIdentifier": "application-universal-identifier-1",
"calendarViewIds": [],
"calendarViewUniversalIdentifiers": [],
"createdAt": "2024-01-01T00:00:00.000Z",
"defaultValue": null,
"description": "default flat field metadata description",
@@ -21,22 +22,30 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a crea
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
"label": "flat field metadata label",
"mainGroupByFieldMetadataViewIds": [],
"mainGroupByFieldMetadataViewUniversalIdentifiers": [],
"morphId": null,
"name": "flatFieldMetadataName",
"objectMetadataId": "object-metadata-id-1",
"objectMetadataUniversalIdentifier": "object-metadata-universal-identifier-1",
"options": null,
"relationTargetFieldMetadataId": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataId": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
"type": "TEXT",
"universalIdentifier": "universal-identifier-1",
"universalSettings": null,
"updatedAt": "2024-01-01T00:00:00.000Z",
"viewFieldIds": [],
"viewFieldUniversalIdentifiers": [],
"viewFilterIds": [],
"viewFilterUniversalIdentifiers": [],
"workspaceId": "workspace-id-1",
},
},
@@ -70,9 +79,10 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a dele
"deletedFlatEntityMaps": {
"byId": {
"field-id-1": {
"__universal": {},
"applicationId": "application-id-1",
"applicationUniversalIdentifier": "application-universal-identifier-1",
"calendarViewIds": [],
"calendarViewUniversalIdentifiers": [],
"createdAt": "2024-01-01T00:00:00.000Z",
"defaultValue": null,
"description": "default flat field metadata description",
@@ -86,22 +96,30 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a dele
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
"label": "flat field metadata label",
"mainGroupByFieldMetadataViewIds": [],
"mainGroupByFieldMetadataViewUniversalIdentifiers": [],
"morphId": null,
"name": "flatFieldMetadataName",
"objectMetadataId": "object-metadata-id-1",
"objectMetadataUniversalIdentifier": "object-metadata-universal-identifier-1",
"options": null,
"relationTargetFieldMetadataId": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataId": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
"type": "TEXT",
"universalIdentifier": "universal-identifier-1",
"universalSettings": null,
"updatedAt": "2024-01-01T00:00:00.000Z",
"viewFieldIds": [],
"viewFieldUniversalIdentifiers": [],
"viewFilterIds": [],
"viewFilterUniversalIdentifiers": [],
"workspaceId": "workspace-id-1",
},
},
@@ -153,9 +171,10 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create
"createdFlatEntityMaps": {
"byId": {
"field-id-3": {
"__universal": {},
"applicationId": "application-id-1",
"applicationUniversalIdentifier": "application-universal-identifier-1",
"calendarViewIds": [],
"calendarViewUniversalIdentifiers": [],
"createdAt": "2024-01-01T00:00:00.000Z",
"defaultValue": null,
"description": "default flat field metadata description",
@@ -169,22 +188,30 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
"label": "flat field metadata label",
"mainGroupByFieldMetadataViewIds": [],
"mainGroupByFieldMetadataViewUniversalIdentifiers": [],
"morphId": null,
"name": "flatFieldMetadataName",
"objectMetadataId": "object-metadata-id-1",
"objectMetadataUniversalIdentifier": "object-metadata-universal-identifier-1",
"options": null,
"relationTargetFieldMetadataId": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataId": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
"type": "TEXT",
"universalIdentifier": "universal-identifier-3",
"universalSettings": null,
"updatedAt": "2024-01-01T00:00:00.000Z",
"viewFieldIds": [],
"viewFieldUniversalIdentifiers": [],
"viewFilterIds": [],
"viewFilterUniversalIdentifiers": [],
"workspaceId": "workspace-id-1",
},
},
@@ -200,9 +227,10 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create
"deletedFlatEntityMaps": {
"byId": {
"field-id-2": {
"__universal": {},
"applicationId": "application-id-1",
"applicationUniversalIdentifier": "application-universal-identifier-1",
"calendarViewIds": [],
"calendarViewUniversalIdentifiers": [],
"createdAt": "2024-01-01T00:00:00.000Z",
"defaultValue": null,
"description": "default flat field metadata description",
@@ -216,22 +244,30 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
"label": "flat field metadata label",
"mainGroupByFieldMetadataViewIds": [],
"mainGroupByFieldMetadataViewUniversalIdentifiers": [],
"morphId": null,
"name": "flatFieldMetadataName",
"objectMetadataId": "object-metadata-id-1",
"objectMetadataUniversalIdentifier": "object-metadata-universal-identifier-1",
"options": null,
"relationTargetFieldMetadataId": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataId": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
"type": "TEXT",
"universalIdentifier": "universal-identifier-2",
"universalSettings": null,
"updatedAt": "2024-01-01T00:00:00.000Z",
"viewFieldIds": [],
"viewFieldUniversalIdentifiers": [],
"viewFilterIds": [],
"viewFilterUniversalIdentifiers": [],
"workspaceId": "workspace-id-1",
},
},
@@ -30,6 +30,10 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
}),
],
metadataName: 'fieldMetadata',
@@ -52,6 +56,10 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
}),
],
to: [],
@@ -76,6 +84,10 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
isActive: false,
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
}),
],
to: [
@@ -89,6 +101,10 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
isActive: true,
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
}),
],
metadataName: 'fieldMetadata',
@@ -111,6 +127,10 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
isActive: true,
}),
getFlatFieldMetadataMock({
@@ -122,6 +142,10 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
}),
],
to: [
@@ -133,6 +157,10 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
workspaceId: 'workspace-id-1',
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
updatedAt: '2024-01-01T00:00:00.000Z',
isActive: false,
}),
@@ -144,6 +172,10 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
workspaceId: 'workspace-id-1',
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
updatedAt: '2024-01-01T00:00:00.000Z',
}),
],
@@ -167,6 +199,10 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
workspaceId: 'workspace-id-1',
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
updatedAt: '2024-01-01T00:00:00.000Z',
}),
],
@@ -1,17 +1,11 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { type DeleteFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
import {
WorkspaceMigrationActionExecutionException,
WorkspaceMigrationActionExecutionExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/generate-column-definitions.util';
import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/get-workspace-schema-context-for-migration.util';
@@ -70,19 +64,9 @@ export class DeleteFieldActionHandlerService extends WorkspaceMigrationRunnerAct
universalIdentifier,
});
const objectMetadataUniversalIdentifier =
fieldMetadata.__universal?.objectMetadataUniversalIdentifier;
if (!isDefined(objectMetadataUniversalIdentifier)) {
throw new WorkspaceMigrationActionExecutionException({
message: `objectMetadataUniversalIdentifier is not defined for field metadata ${universalIdentifier}`,
code: WorkspaceMigrationActionExecutionExceptionCode.NOT_SUPPORTED,
});
}
const flatObjectMetadata = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: objectMetadataUniversalIdentifier,
universalIdentifier: fieldMetadata.objectMetadataUniversalIdentifier,
});
const { schemaName, tableName } = getWorkspaceSchemaContextForMigration({
@@ -97,19 +97,10 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
flatEntityMaps: flatFieldMetadataMaps,
});
const objectMetadataUniversalIdentifier =
currentFlatFieldMetadata.__universal?.objectMetadataUniversalIdentifier;
if (!isDefined(objectMetadataUniversalIdentifier)) {
throw new WorkspaceMigrationActionExecutionException({
message: `objectMetadataUniversalIdentifier is not defined for field metadata ${entityId}`,
code: WorkspaceMigrationActionExecutionExceptionCode.NOT_SUPPORTED,
});
}
const flatObjectMetadata = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: objectMetadataUniversalIdentifier,
universalIdentifier:
currentFlatFieldMetadata.objectMetadataUniversalIdentifier,
});
const { schemaName, tableName } = getWorkspaceSchemaContextForMigration({
@@ -25,6 +25,7 @@ export const extractRecordIdsAndDatesAsExpectAny = (
if (
key.endsWith('Id') ||
key.endsWith('UniversalIdentifier') ||
key === 'universalIdentifier' ||
key === 'id' ||
key === 'updatedAt' ||
@@ -0,0 +1,3 @@
export type IsEmptyObject<T extends object> = [keyof T] extends [never]
? true
: false;
@@ -0,0 +1 @@
export type IsNever<T> = [T] extends [never] ? true : false;
@@ -105,8 +105,10 @@ export { FILTERABLE_FIELD_TYPES } from './FilterableFieldType';
export { FirstDayOfTheWeek } from './FirstDayOfTheWeek';
export type { FromTo } from './FromToType';
export { HTTPMethod } from './HttpMethod';
export type { IsEmptyObject } from './IsEmptyObject.type';
export type { IsEmptyRecord } from './IsEmptyRecord.type';
export type { IsExactly } from './IsExactly';
export type { IsNever } from './IsNever.type';
export type { IsSerializedRelation } from './IsSerializedRelation.type';
export type { LogicFunctionEvent } from './LogicFunctionEvent';
export { MessageParticipantRole } from './MessageParticipantRole';