feat: new relation sync-metadata, twenty-orm, create/update (#10217)

Fix
https://github.com/twentyhq/core-team-issues/issues/330#issue-2827026606
and
https://github.com/twentyhq/core-team-issues/issues/327#issue-2827001814

What this PR does when `isNewRelationEnabled` is set to `true`:
- [x] Drop the creation of the  foreign key as a `FieldMetadata`
- [x] Stop creating `RelationMetadata`
- [x] Properly fill `FieldMetadata` of type `RELATION` during the sync
command
- [x] Use new relation settings in TwentyORM
- [x] Properly create `FieldMetadata` relations when we create a new
object
- [x] Handle `database:reset` with new relations

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
Jérémy M
2025-04-22 19:01:39 +02:00
committed by GitHub
parent de1489aabb
commit cc29c25176
160 changed files with 3247 additions and 711 deletions

View File

@ -14,7 +14,8 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
it('should return fieldName if simpleValue', async () => { it('should return fieldName if simpleValue', async () => {
const res = mapFieldMetadataToGraphQLQuery({ const res = mapFieldMetadataToGraphQLQuery({
objectMetadataItems: generatedMockObjectMetadataItems, objectMetadataItems: generatedMockObjectMetadataItems,
field: personObjectMetadataItem.fields.find( gqlField: 'id',
fieldMetadata: personObjectMetadataItem.fields.find(
(field) => field.name === 'id', (field) => field.name === 'id',
)!, )!,
}); });
@ -23,7 +24,8 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
it('should return fieldName if composite', async () => { it('should return fieldName if composite', async () => {
const res = mapFieldMetadataToGraphQLQuery({ const res = mapFieldMetadataToGraphQLQuery({
objectMetadataItems: generatedMockObjectMetadataItems, objectMetadataItems: generatedMockObjectMetadataItems,
field: personObjectMetadataItem.fields.find( gqlField: 'name',
fieldMetadata: personObjectMetadataItem.fields.find(
(field) => field.name === 'name', (field) => field.name === 'name',
)!, )!,
}); });
@ -39,7 +41,8 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
it('should return non relation subFields if relation', async () => { it('should return non relation subFields if relation', async () => {
const res = mapFieldMetadataToGraphQLQuery({ const res = mapFieldMetadataToGraphQLQuery({
objectMetadataItems: generatedMockObjectMetadataItems, objectMetadataItems: generatedMockObjectMetadataItems,
field: personObjectMetadataItem.fields.find( gqlField: 'company',
fieldMetadata: personObjectMetadataItem.fields.find(
(field) => field.name === 'company', (field) => field.name === 'company',
)!, )!,
}); });
@ -115,7 +118,8 @@ idealCustomerProfile
id: true, id: true,
idealCustomerProfile: true, idealCustomerProfile: true,
}, },
field: personObjectMetadataItem.fields.find( gqlField: 'company',
fieldMetadata: personObjectMetadataItem.fields.find(
(field) => field.name === 'company', (field) => field.name === 'company',
)!, )!,
}); });

View File

@ -5,14 +5,16 @@ describe('shouldFieldBeQueried', () => {
describe('if recordGqlFields is absent, we query all except relations', () => { describe('if recordGqlFields is absent, we query all except relations', () => {
it('should be queried if the field is not a relation', () => { it('should be queried if the field is not a relation', () => {
const res = shouldFieldBeQueried({ const res = shouldFieldBeQueried({
field: { name: 'fieldName', type: FieldMetadataType.BOOLEAN }, gqlField: 'fieldName',
fieldMetadata: { name: 'fieldName', type: FieldMetadataType.BOOLEAN },
}); });
expect(res).toBe(true); expect(res).toBe(true);
}); });
it('should not be queried if the field is a relation', () => { it('should not be queried if the field is a relation', () => {
const res = shouldFieldBeQueried({ const res = shouldFieldBeQueried({
field: { name: 'fieldName', type: FieldMetadataType.RELATION }, gqlField: 'fieldName',
fieldMetadata: { name: 'fieldName', type: FieldMetadataType.RELATION },
}); });
expect(res).toBe(false); expect(res).toBe(false);
}); });
@ -21,8 +23,9 @@ describe('shouldFieldBeQueried', () => {
describe('if recordGqlFields is present, we respect it', () => { describe('if recordGqlFields is present, we respect it', () => {
it('should be queried if true', () => { it('should be queried if true', () => {
const res = shouldFieldBeQueried({ const res = shouldFieldBeQueried({
gqlField: 'fieldName',
fieldMetadata: { name: 'fieldName', type: FieldMetadataType.RELATION },
recordGqlFields: { fieldName: true }, recordGqlFields: { fieldName: true },
field: { name: 'fieldName', type: FieldMetadataType.RELATION },
}); });
expect(res).toBe(true); expect(res).toBe(true);
}); });
@ -30,23 +33,26 @@ describe('shouldFieldBeQueried', () => {
it('should be queried if object', () => { it('should be queried if object', () => {
const res = shouldFieldBeQueried({ const res = shouldFieldBeQueried({
recordGqlFields: { fieldName: { subFieldName: false } }, recordGqlFields: { fieldName: { subFieldName: false } },
field: { name: 'fieldName', type: FieldMetadataType.RELATION }, fieldMetadata: { name: 'fieldName', type: FieldMetadataType.RELATION },
gqlField: 'fieldName',
}); });
expect(res).toBe(true); expect(res).toBe(true);
}); });
it('should not be queried if false', () => { it('should not be queried if false', () => {
const res = shouldFieldBeQueried({ const res = shouldFieldBeQueried({
gqlField: 'fieldName',
fieldMetadata: { name: 'fieldName', type: FieldMetadataType.RELATION },
recordGqlFields: { fieldName: false }, recordGqlFields: { fieldName: false },
field: { name: 'fieldName', type: FieldMetadataType.RELATION },
}); });
expect(res).toBe(false); expect(res).toBe(false);
}); });
it('should not be queried if absent', () => { it('should not be queried if absent', () => {
const res = shouldFieldBeQueried({ const res = shouldFieldBeQueried({
gqlField: 'fieldName',
fieldMetadata: { name: 'fieldName', type: FieldMetadataType.RELATION },
recordGqlFields: { otherFieldName: false }, recordGqlFields: { otherFieldName: false },
field: { name: 'fieldName', type: FieldMetadataType.RELATION },
}); });
expect(res).toBe(false); expect(res).toBe(false);
}); });

View File

@ -12,40 +12,50 @@ import { FieldMetadataItem } from '../types/FieldMetadataItem';
type MapFieldMetadataToGraphQLQueryArgs = { type MapFieldMetadataToGraphQLQueryArgs = {
objectMetadataItems: ObjectMetadataItem[]; objectMetadataItems: ObjectMetadataItem[];
field: Pick<FieldMetadataItem, 'name' | 'type' | 'relationDefinition'>; gqlField: string;
fieldMetadata: Pick<
FieldMetadataItem,
'name' | 'type' | 'relationDefinition' | 'settings'
>;
relationRecordGqlFields?: RecordGqlFields; relationRecordGqlFields?: RecordGqlFields;
computeReferences?: boolean; computeReferences?: boolean;
}; };
// TODO: change ObjectMetadataItems mock before refactoring with relationDefinition computed field // TODO: change ObjectMetadataItems mock before refactoring with relationDefinition computed field
export const mapFieldMetadataToGraphQLQuery = ({ export const mapFieldMetadataToGraphQLQuery = ({
objectMetadataItems, objectMetadataItems,
field, gqlField,
fieldMetadata,
relationRecordGqlFields, relationRecordGqlFields,
computeReferences = false, computeReferences = false,
}: MapFieldMetadataToGraphQLQueryArgs): string => { }: MapFieldMetadataToGraphQLQueryArgs): string => {
const fieldType = field.type; const fieldType = fieldMetadata.type;
const fieldIsNonCompositeField = isNonCompositeField(fieldType); const fieldIsNonCompositeField = isNonCompositeField(fieldType);
if (fieldIsNonCompositeField) { if (fieldIsNonCompositeField) {
return field.name; return gqlField;
} }
if ( if (
fieldType === FieldMetadataType.RELATION && fieldType === FieldMetadataType.RELATION &&
field.relationDefinition?.direction === RelationDefinitionType.MANY_TO_ONE fieldMetadata.relationDefinition?.direction ===
RelationDefinitionType.MANY_TO_ONE
) { ) {
const relationMetadataItem = objectMetadataItems.find( const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => (objectMetadataItem) =>
objectMetadataItem.id === objectMetadataItem.id ===
field.relationDefinition?.targetObjectMetadata.id, fieldMetadata.relationDefinition?.targetObjectMetadata.id,
); );
if (isUndefined(relationMetadataItem)) { if (isUndefined(relationMetadataItem)) {
return ''; return '';
} }
return `${field.name} if (gqlField === fieldMetadata.settings?.joinColumnName) {
return `${gqlField}`;
}
return `${gqlField}
${mapObjectMetadataToGraphQLQuery({ ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems, objectMetadataItems,
objectMetadataItem: relationMetadataItem, objectMetadataItem: relationMetadataItem,
@ -57,19 +67,20 @@ ${mapObjectMetadataToGraphQLQuery({
if ( if (
fieldType === FieldMetadataType.RELATION && fieldType === FieldMetadataType.RELATION &&
field.relationDefinition?.direction === RelationDefinitionType.ONE_TO_MANY fieldMetadata.relationDefinition?.direction ===
RelationDefinitionType.ONE_TO_MANY
) { ) {
const relationMetadataItem = objectMetadataItems.find( const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => (objectMetadataItem) =>
objectMetadataItem.id === objectMetadataItem.id ===
field.relationDefinition?.targetObjectMetadata.id, fieldMetadata.relationDefinition?.targetObjectMetadata.id,
); );
if (isUndefined(relationMetadataItem)) { if (isUndefined(relationMetadataItem)) {
return ''; return '';
} }
return `${field.name} return `${gqlField}
{ {
edges { edges {
node ${mapObjectMetadataToGraphQLQuery({ node ${mapObjectMetadataToGraphQLQuery({
@ -84,7 +95,7 @@ ${mapObjectMetadataToGraphQLQuery({
} }
if (fieldType === FieldMetadataType.LINKS) { if (fieldType === FieldMetadataType.LINKS) {
return `${field.name} return `${gqlField}
{ {
primaryLinkUrl primaryLinkUrl
primaryLinkLabel primaryLinkLabel
@ -93,7 +104,7 @@ ${mapObjectMetadataToGraphQLQuery({
} }
if (fieldType === FieldMetadataType.CURRENCY) { if (fieldType === FieldMetadataType.CURRENCY) {
return `${field.name} return `${gqlField}
{ {
amountMicros amountMicros
currencyCode currencyCode
@ -102,7 +113,7 @@ ${mapObjectMetadataToGraphQLQuery({
} }
if (fieldType === FieldMetadataType.FULL_NAME) { if (fieldType === FieldMetadataType.FULL_NAME) {
return `${field.name} return `${gqlField}
{ {
firstName firstName
lastName lastName
@ -110,7 +121,7 @@ ${mapObjectMetadataToGraphQLQuery({
} }
if (fieldType === FieldMetadataType.ADDRESS) { if (fieldType === FieldMetadataType.ADDRESS) {
return `${field.name} return `${gqlField}
{ {
addressStreet1 addressStreet1
addressStreet2 addressStreet2
@ -124,7 +135,7 @@ ${mapObjectMetadataToGraphQLQuery({
} }
if (fieldType === FieldMetadataType.ACTOR) { if (fieldType === FieldMetadataType.ACTOR) {
return `${field.name} return `${gqlField}
{ {
source source
workspaceMemberId workspaceMemberId
@ -134,7 +145,7 @@ ${mapObjectMetadataToGraphQLQuery({
} }
if (fieldType === FieldMetadataType.EMAILS) { if (fieldType === FieldMetadataType.EMAILS) {
return `${field.name} return `${gqlField}
{ {
primaryEmail primaryEmail
additionalEmails additionalEmails
@ -142,7 +153,7 @@ ${mapObjectMetadataToGraphQLQuery({
} }
if (fieldType === FieldMetadataType.PHONES) { if (fieldType === FieldMetadataType.PHONES) {
return `${field.name} return `${gqlField}
{ {
primaryPhoneNumber primaryPhoneNumber
primaryPhoneCountryCode primaryPhoneCountryCode
@ -152,7 +163,7 @@ ${mapObjectMetadataToGraphQLQuery({
} }
if (fieldType === FieldMetadataType.RICH_TEXT_V2) { if (fieldType === FieldMetadataType.RICH_TEXT_V2) {
return `${field.name} return `${gqlField}
{ {
blocknote blocknote
markdown markdown

View File

@ -3,6 +3,8 @@ import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapField
import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueried'; import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueried';
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode'; import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
type MapObjectMetadataToGraphQLQueryArgs = { type MapObjectMetadataToGraphQLQueryArgs = {
objectMetadataItems: ObjectMetadataItem[]; objectMetadataItems: ObjectMetadataItem[];
@ -18,16 +20,40 @@ export const mapObjectMetadataToGraphQLQuery = ({
computeReferences = false, computeReferences = false,
isRootLevel = true, isRootLevel = true,
}: MapObjectMetadataToGraphQLQueryArgs): string => { }: MapObjectMetadataToGraphQLQueryArgs): string => {
const fieldsThatShouldBeQueried = const manyToOneRelationFields = objectMetadataItem?.fields
objectMetadataItem?.fields .filter((field) => field.isActive)
.filter((field) => field.isActive) .filter((field) => field.type === FieldMetadataType.RELATION)
.sort((fieldA, fieldB) => fieldA.name.localeCompare(fieldB.name)) .filter((field) => isDefined(field.settings?.joinColumnName));
.filter((field) =>
const manyToOneRelationGqlFieldWithFieldMetadata =
manyToOneRelationFields.map((field) => ({
gqlField: field.settings?.joinColumnName,
fieldMetadata: field,
}));
const gqlFieldWithFieldMetadataThatCouldBeQueried = [
...objectMetadataItem.fields
.filter((fieldMetadata) => fieldMetadata.isActive)
.map((fieldMetadata) => ({
gqlField: fieldMetadata.name,
fieldMetadata,
})),
...manyToOneRelationGqlFieldWithFieldMetadata,
].sort((gqlFieldWithFieldMetadataA, gqlFieldWithFieldMetadataB) =>
gqlFieldWithFieldMetadataA.gqlField.localeCompare(
gqlFieldWithFieldMetadataB.gqlField,
),
);
const gqlFieldWithFieldMetadataThatSouldBeQueried =
gqlFieldWithFieldMetadataThatCouldBeQueried.filter(
(gqlFieldWithFieldMetadata) =>
shouldFieldBeQueried({ shouldFieldBeQueried({
field, gqlField: gqlFieldWithFieldMetadata.gqlField,
fieldMetadata: gqlFieldWithFieldMetadata.fieldMetadata,
recordGqlFields, recordGqlFields,
}), }),
) ?? []; );
if (!isRootLevel && computeReferences) { if (!isRootLevel && computeReferences) {
return `{ return `{
@ -37,9 +63,10 @@ export const mapObjectMetadataToGraphQLQuery = ({
return `{ return `{
__typename __typename
${fieldsThatShouldBeQueried ${gqlFieldWithFieldMetadataThatSouldBeQueried
.map((field) => { .map((gqlFieldWithFieldMetadata) => {
const currentRecordGqlFields = recordGqlFields?.[field.name]; const currentRecordGqlFields =
recordGqlFields?.[gqlFieldWithFieldMetadata.gqlField];
const relationRecordGqlFields = isRecordGqlFieldsNode( const relationRecordGqlFields = isRecordGqlFieldsNode(
currentRecordGqlFields, currentRecordGqlFields,
) )
@ -47,7 +74,8 @@ ${fieldsThatShouldBeQueried
: undefined; : undefined;
return mapFieldMetadataToGraphQLQuery({ return mapFieldMetadataToGraphQLQuery({
objectMetadataItems, objectMetadataItems,
field, gqlField: gqlFieldWithFieldMetadata.gqlField,
fieldMetadata: gqlFieldWithFieldMetadata.fieldMetadata,
relationRecordGqlFields, relationRecordGqlFields,
computeReferences, computeReferences,
}); });

View File

@ -3,27 +3,30 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
export const shouldFieldBeQueried = ({ export const shouldFieldBeQueried = ({
field, gqlField,
fieldMetadata,
recordGqlFields, recordGqlFields,
}: { }: {
field: Pick<FieldMetadataItem, 'name' | 'type'>; gqlField: string;
fieldMetadata: Pick<FieldMetadataItem, 'name' | 'type'>;
objectRecord?: ObjectRecord; objectRecord?: ObjectRecord;
recordGqlFields?: RecordGqlOperationGqlRecordFields; recordGqlFields?: RecordGqlOperationGqlRecordFields;
}): any => { }): any => {
if ( if (
isUndefinedOrNull(recordGqlFields) && isUndefinedOrNull(recordGqlFields) &&
field.type !== FieldMetadataType.RELATION fieldMetadata.type !== FieldMetadataType.RELATION
) { ) {
return true; return true;
} }
if ( if (
isDefined(recordGqlFields) && isDefined(recordGqlFields) &&
isDefined(recordGqlFields[field.name]) && isDefined(recordGqlFields[gqlField]) &&
recordGqlFields[field.name] !== false recordGqlFields[gqlField] !== false
) { ) {
return true; return true;
} }

View File

@ -7,12 +7,12 @@ import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getR
import { getRefName } from '@/object-record/cache/utils/getRefName'; import { getRefName } from '@/object-record/cache/utils/getRefName';
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { import {
FieldMetadataType, FieldMetadataType,
RelationDefinitionType, RelationDefinitionType,
} from '~/generated-metadata/graphql'; } from '~/generated-metadata/graphql';
import { pascalCase } from '~/utils/string/pascalCase'; import { pascalCase } from '~/utils/string/pascalCase';
import { isDefined } from 'twenty-shared/utils';
export const getRecordNodeFromRecord = <T extends ObjectRecord>({ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
objectMetadataItems, objectMetadataItems,
@ -51,9 +51,11 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
return undefined; return undefined;
} }
const field = objectMetadataItem.fields.find( const field =
(field) => field.name === fieldName, objectMetadataItem.fields.find((field) => field.name === fieldName) ??
); objectMetadataItem.fields.find(
(field) => field.settings?.joinColumnName === fieldName,
);
if (isUndefined(field)) { if (isUndefined(field)) {
return undefined; return undefined;
@ -94,6 +96,12 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
switch (field.type) { switch (field.type) {
case FieldMetadataType.RELATION: { case FieldMetadataType.RELATION: {
const isJoinColumn = field.settings?.joinColumnName === fieldName;
if (isJoinColumn) {
return [fieldName, value];
}
if ( if (
isUndefined( isUndefined(
field.relationDefinition?.targetObjectMetadata.nameSingular, field.relationDefinition?.targetObjectMetadata.nameSingular,

View File

@ -6,8 +6,8 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObje
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const updateRecordFromCache = <T extends ObjectRecord>({ export const updateRecordFromCache = <T extends ObjectRecord>({
objectMetadataItems, objectMetadataItems,

View File

@ -1,4 +1,5 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isDefined } from 'twenty-shared/utils';
export type GenerateDepthOneRecordGqlFields = { export type GenerateDepthOneRecordGqlFields = {
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
@ -10,6 +11,11 @@ export const generateDepthOneRecordGqlFields = ({
objectMetadataItem.fields.reduce<Record<string, true>>((acc, field) => { objectMetadataItem.fields.reduce<Record<string, true>>((acc, field) => {
return { return {
...acc, ...acc,
...(isDefined(field.settings?.joinColumnName)
? {
[field.settings.joinColumnName]: true,
}
: {}),
[field.name]: true, [field.name]: true,
}; };
}, {}); }, {});

View File

@ -18,8 +18,8 @@ import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/g
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { isNull } from '@sniptt/guards'; import { isNull } from '@sniptt/guards';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue';
type useUpdateOneRecordProps = { type useUpdateOneRecordProps = {
objectNameSingular: string; objectNameSingular: string;
@ -91,6 +91,7 @@ export const useUpdateOneRecord = <
id: idToUpdate, id: idToUpdate,
__typename: getObjectTypename(objectMetadataItem.nameSingular), __typename: getObjectTypename(objectMetadataItem.nameSingular),
}; };
const optimisticRecordWithConnection = const optimisticRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({ getRecordNodeFromRecord<ObjectRecord>({
record: computedOptimisticRecord, record: computedOptimisticRecord,
@ -110,6 +111,7 @@ export const useUpdateOneRecord = <
objectMetadataItem, objectMetadataItem,
record: optimisticRecordInput, record: optimisticRecordInput,
}); });
updateRecordFromCache({ updateRecordFromCache({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,

View File

@ -38,9 +38,9 @@ import { isMatchingRichTextV2Filter } from '@/object-record/record-filter/utils/
import { isMatchingSelectFilter } from '@/object-record/record-filter/utils/isMatchingSelectFilter'; import { isMatchingSelectFilter } from '@/object-record/record-filter/utils/isMatchingSelectFilter';
import { isMatchingStringFilter } from '@/object-record/record-filter/utils/isMatchingStringFilter'; import { isMatchingStringFilter } from '@/object-record/record-filter/utils/isMatchingStringFilter';
import { isMatchingUUIDFilter } from '@/object-record/record-filter/utils/isMatchingUUIDFilter'; import { isMatchingUUIDFilter } from '@/object-record/record-filter/utils/isMatchingUUIDFilter';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isEmptyObject } from '~/utils/isEmptyObject'; import { isEmptyObject } from '~/utils/isEmptyObject';
import { isDefined } from 'twenty-shared/utils';
const isLeafFilter = ( const isLeafFilter = (
filter: RecordGqlOperationFilter, filter: RecordGqlOperationFilter,
@ -167,9 +167,13 @@ export const isRecordMatchingFilter = ({
if (isEmptyObject(filterValue)) return true; if (isEmptyObject(filterValue)) return true;
const objectMetadataField = objectMetadataItem.fields.find( const objectMetadataField =
(field) => field.name === filterKey, objectMetadataItem.fields.find((field) => field.name === filterKey) ??
); objectMetadataItem.fields.find(
(field) =>
field.type === FieldMetadataType.RELATION &&
field.settings?.joinColumnName === filterKey,
);
if (!isDefined(objectMetadataField)) { if (!isDefined(objectMetadataField)) {
throw new Error( throw new Error(
@ -359,6 +363,16 @@ export const isRecordMatchingFilter = ({
}); });
} }
case FieldMetadataType.RELATION: { case FieldMetadataType.RELATION: {
const isJoinColumn =
objectMetadataField.settings?.joinColumnName === filterKey;
if (isJoinColumn) {
return isMatchingUUIDFilter({
uuidFilter: filterValue as UUIDFilter,
value: record[filterKey],
});
}
throw new Error( throw new Error(
`Not implemented yet, use UUID filter instead on the corredponding "${filterKey}Id" field`, `Not implemented yet, use UUID filter instead on the corredponding "${filterKey}Id" field`,
); );

View File

@ -14,9 +14,9 @@ import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUu
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { buildOptimisticActorFieldValueFromCurrentWorkspaceMember } from '@/object-record/utils/buildOptimisticActorFieldValueFromCurrentWorkspaceMember'; import { buildOptimisticActorFieldValueFromCurrentWorkspaceMember } from '@/object-record/utils/buildOptimisticActorFieldValueFromCurrentWorkspaceMember';
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName'; import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
import { isDefined } from 'twenty-shared/utils';
import { RelationDefinitionType } from '~/generated-metadata/graphql'; import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldMetadataType } from '~/generated/graphql'; import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils';
type ComputeOptimisticCacheRecordInputArgs = { type ComputeOptimisticCacheRecordInputArgs = {
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
@ -32,11 +32,23 @@ export const computeOptimisticRecordFromInput = ({
}: ComputeOptimisticCacheRecordInputArgs) => { }: ComputeOptimisticCacheRecordInputArgs) => {
const unknownRecordInputFields = Object.keys(recordInput).filter( const unknownRecordInputFields = Object.keys(recordInput).filter(
(recordKey) => { (recordKey) => {
const isUnknownMetadataItemField = const correspondingFieldMetadataItem = objectMetadataItem.fields.find(
objectMetadataItem.fields.find((field) => field.name === recordKey) === (field) => field.name === recordKey,
undefined; );
const potentialJoinColumnNameFieldMetadataItem =
objectMetadataItem.fields.find(
(field) =>
field.type === FieldMetadataType.RELATION &&
field.settings?.joinColumnName === recordKey,
);
const isUnknownField =
!isDefined(correspondingFieldMetadataItem) &&
!isDefined(potentialJoinColumnNameFieldMetadataItem);
const isTypenameField = recordKey === GRAPHQL_TYPENAME_KEY; const isTypenameField = recordKey === GRAPHQL_TYPENAME_KEY;
return isUnknownMetadataItemField && !isTypenameField; return isUnknownField && !isTypenameField;
}, },
); );
if (unknownRecordInputFields.length > 0) { if (unknownRecordInputFields.length > 0) {
@ -122,6 +134,7 @@ export const computeOptimisticRecordFromInput = ({
const relationFieldIdName = getForeignKeyNameFromRelationFieldName( const relationFieldIdName = getForeignKeyNameFromRelationFieldName(
fieldMetadataItem.name, fieldMetadataItem.name,
); );
const recordInputFieldIdValue: string | null | undefined = const recordInputFieldIdValue: string | null | undefined =
recordInput[relationFieldIdName]; recordInput[relationFieldIdName];
@ -132,7 +145,11 @@ export const computeOptimisticRecordFromInput = ({
const relationIdFieldMetadataItem = objectMetadataItem.fields.find( const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.name === relationFieldIdName, (field) => field.name === relationFieldIdName,
); );
if (!isDefined(relationIdFieldMetadataItem)) {
if (
!isDefined(relationIdFieldMetadataItem) &&
!isDefined(fieldMetadataItem.settings?.joinColumnName)
) {
throw new Error( throw new Error(
'Should never occur, encountered unknown relationId within relations definitions', 'Should never occur, encountered unknown relationId within relations definitions',
); );

View File

@ -1,8 +1,8 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { RelationDefinitionType } from '~/generated-metadata/graphql'; import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldMetadataType } from '~/generated/graphql'; import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils';
export const sanitizeRecordInput = ({ export const sanitizeRecordInput = ({
objectMetadataItem, objectMetadataItem,
@ -17,14 +17,26 @@ export const sanitizeRecordInput = ({
const fieldMetadataItem = objectMetadataItem.fields.find( const fieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.name === fieldName, (field) => field.name === fieldName,
); );
const potentialJoinColumnNameFieldMetadataItem =
objectMetadataItem.fields.find(
(field) =>
field.type === FieldMetadataType.RELATION &&
field.settings?.joinColumnName === fieldName,
);
if (!fieldMetadataItem) return undefined; if (
!isDefined(fieldMetadataItem) &&
!isDefined(potentialJoinColumnNameFieldMetadataItem)
) {
return undefined;
}
if (!fieldMetadataItem.isNullable && fieldValue == null) { if (fieldMetadataItem?.isNullable === false && fieldValue == null) {
return undefined; return undefined;
} }
if ( if (
isDefined(fieldMetadataItem) &&
fieldMetadataItem.type === FieldMetadataType.RELATION && fieldMetadataItem.type === FieldMetadataType.RELATION &&
fieldMetadataItem.relationDefinition?.direction === fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.MANY_TO_ONE RelationDefinitionType.MANY_TO_ONE
@ -42,6 +54,7 @@ export const sanitizeRecordInput = ({
} }
if ( if (
isDefined(fieldMetadataItem) &&
fieldMetadataItem.type === FieldMetadataType.RELATION && fieldMetadataItem.type === FieldMetadataType.RELATION &&
fieldMetadataItem.relationDefinition?.direction === fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.ONE_TO_MANY RelationDefinitionType.ONE_TO_MANY

View File

@ -14,9 +14,9 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { H2Title } from 'twenty-ui/display'; import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout'; import { Section } from 'twenty-ui/layout';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const SettingsAccounts = () => { export const SettingsAccounts = () => {
const { t } = useLingui(); const { t } = useLingui();

View File

@ -4,6 +4,7 @@ PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/default
REDIS_URL=redis://localhost:6379 REDIS_URL=redis://localhost:6379
APP_SECRET=replace_me_with_a_random_string APP_SECRET=replace_me_with_a_random_string
SIGN_IN_PREFILLED=true SIGN_IN_PREFILLED=true
SYNC_METADATA_INDEX_ENABLED=false
FRONTEND_URL=http://localhost:3001 FRONTEND_URL=http://localhost:3001

View File

@ -18,10 +18,10 @@ import {
RelationDirection, RelationDirection,
deduceRelationDirection, deduceRelationDirection,
} from 'src/engine/utils/deduce-relation-direction.util'; } from 'src/engine/utils/deduce-relation-direction.util';
import { isFieldMetadataOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
@Command({ @Command({
name: 'upgrade:0-51:migrate-relations-to-field-metadata', name: 'upgrade:0-52:migrate-relations-to-field-metadata',
description: 'Migrate relations to field metadata', description: 'Migrate relations to field metadata',
}) })
export class MigrateRelationsToFieldMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { export class MigrateRelationsToFieldMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
@ -64,13 +64,13 @@ export class MigrateRelationsToFieldMetadataCommand extends ActiveOrSuspendedWor
const joinColumnFieldMetadataCollection = fieldMetadataCollection.filter( const joinColumnFieldMetadataCollection = fieldMetadataCollection.filter(
(fieldMetadata) => (fieldMetadata) =>
isFieldMetadataOfType(fieldMetadata, FieldMetadataType.UUID), isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.UUID),
// TODO: Fix this, it's working in other places but not here // TODO: Fix this, it's working in other places but not here
) as FieldMetadataEntity<FieldMetadataType.UUID>[]; ) as FieldMetadataEntity<FieldMetadataType.UUID>[];
const fieldMetadataToUpdateCollection = fieldMetadataCollection const fieldMetadataToUpdateCollection = fieldMetadataCollection
.filter((fieldMetadata) => .filter((fieldMetadata) =>
isFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION), isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION),
) )
.map((fieldMetadata) => .map((fieldMetadata) =>
this.updateRelationFieldMetadata( this.updateRelationFieldMetadata(

View File

@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { BackfillWorkflowNextStepIdsCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-backfill-workflow-next-step-ids.command'; import { BackfillWorkflowNextStepIdsCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-backfill-workflow-next-step-ids.command';
import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-migrate-relations-to-field-metadata.command';
import { UpgradeDateAndDateTimeFieldsSettingsJsonCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-upgrade-settings-field'; import { UpgradeDateAndDateTimeFieldsSettingsJsonCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-upgrade-settings-field';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -20,10 +21,12 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
providers: [ providers: [
BackfillWorkflowNextStepIdsCommand, BackfillWorkflowNextStepIdsCommand,
UpgradeDateAndDateTimeFieldsSettingsJsonCommand, UpgradeDateAndDateTimeFieldsSettingsJsonCommand,
MigrateRelationsToFieldMetadataCommand,
], ],
exports: [ exports: [
BackfillWorkflowNextStepIdsCommand, BackfillWorkflowNextStepIdsCommand,
UpgradeDateAndDateTimeFieldsSettingsJsonCommand, UpgradeDateAndDateTimeFieldsSettingsJsonCommand,
MigrateRelationsToFieldMetadataCommand,
], ],
}) })
export class V0_52_UpgradeVersionCommandModule {} export class V0_52_UpgradeVersionCommandModule {}

View File

@ -17,6 +17,7 @@ import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/data
import { InitializePermissionsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-initialize-permissions.command'; import { InitializePermissionsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-initialize-permissions.command';
import { UpdateViewAggregateOperationsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-update-view-aggregate-operations.command'; import { UpdateViewAggregateOperationsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-update-view-aggregate-operations.command';
import { UpgradeCreatedByEnumCommand } from 'src/database/commands/upgrade-version-command/0-51/0-51-update-workflow-trigger-type-enum.command'; import { UpgradeCreatedByEnumCommand } from 'src/database/commands/upgrade-version-command/0-51/0-51-update-workflow-trigger-type-enum.command';
import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-migrate-relations-to-field-metadata.command';
import { UpgradeDateAndDateTimeFieldsSettingsJsonCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-upgrade-settings-field'; import { UpgradeDateAndDateTimeFieldsSettingsJsonCommand } from 'src/database/commands/upgrade-version-command/0-52/0-52-upgrade-settings-field';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@ -58,6 +59,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
// 0.52 Commands // 0.52 Commands
protected readonly upgradeDateAndDateTimeFieldsSettingsJsonCommand: UpgradeDateAndDateTimeFieldsSettingsJsonCommand, protected readonly upgradeDateAndDateTimeFieldsSettingsJsonCommand: UpgradeDateAndDateTimeFieldsSettingsJsonCommand,
protected readonly migrateRelationsToFieldMetadataCommand: MigrateRelationsToFieldMetadataCommand,
) { ) {
super( super(
workspaceRepository, workspaceRepository,
@ -99,6 +101,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
const _commands_052: VersionCommands = { const _commands_052: VersionCommands = {
beforeSyncMetadata: [ beforeSyncMetadata: [
this.upgradeDateAndDateTimeFieldsSettingsJsonCommand, this.upgradeDateAndDateTimeFieldsSettingsJsonCommand,
this.migrateRelationsToFieldMetadataCommand,
], ],
afterSyncMetadata: [], afterSyncMetadata: [],
}; };

View File

@ -63,7 +63,7 @@ export const seedFeatureFlags = async (
{ {
key: FeatureFlagKey.IsNewRelationEnabled, key: FeatureFlagKey.IsNewRelationEnabled,
workspaceId: workspaceId, workspaceId: workspaceId,
value: false, value: true,
}, },
]) ])
.execute(); .execute();

View File

@ -29,6 +29,7 @@ export const mockPersonObjectMetadata = (
fields: [], fields: [],
indexMetadatas: [], indexMetadatas: [],
fieldsById: {}, fieldsById: {},
fieldsByJoinColumnName: {},
fieldsByName: { fieldsByName: {
name: { name: {
id: '', id: '',

View File

@ -14,15 +14,19 @@ import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.pars
export class GraphqlQueryFilterConditionParser { export class GraphqlQueryFilterConditionParser {
private fieldMetadataMapByName: FieldMetadataMap; private fieldMetadataMapByName: FieldMetadataMap;
private fieldMetadataMapByJoinColumnName: FieldMetadataMap;
private queryFilterFieldParser: GraphqlQueryFilterFieldParser; private queryFilterFieldParser: GraphqlQueryFilterFieldParser;
constructor( constructor(
fieldMetadataMapByName: FieldMetadataMap, fieldMetadataMapByName: FieldMetadataMap,
fieldMetadataMapByJoinColumnName: FieldMetadataMap,
featureFlagsMap: FeatureFlagMap, featureFlagsMap: FeatureFlagMap,
) { ) {
this.fieldMetadataMapByName = fieldMetadataMapByName; this.fieldMetadataMapByName = fieldMetadataMapByName;
this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName;
this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser( this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser(
this.fieldMetadataMapByName, this.fieldMetadataMapByName,
this.fieldMetadataMapByJoinColumnName,
featureFlagsMap, featureFlagsMap,
); );
} }

View File

@ -1,5 +1,5 @@
import { WhereExpressionBuilder } from 'typeorm';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
import { WhereExpressionBuilder } from 'typeorm';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
@ -18,13 +18,16 @@ const ARRAY_OPERATORS = ['in', 'contains', 'notContains'];
export class GraphqlQueryFilterFieldParser { export class GraphqlQueryFilterFieldParser {
private fieldMetadataMapByName: FieldMetadataMap; private fieldMetadataMapByName: FieldMetadataMap;
private fieldMetadataMapByJoinColumnName: FieldMetadataMap;
private featureFlagsMap: FeatureFlagMap; private featureFlagsMap: FeatureFlagMap;
constructor( constructor(
fieldMetadataMapByName: FieldMetadataMap, fieldMetadataMapByName: FieldMetadataMap,
fieldMetadataMapByJoinColumnName: FieldMetadataMap,
featureFlagsMap: FeatureFlagMap, featureFlagsMap: FeatureFlagMap,
) { ) {
this.fieldMetadataMapByName = fieldMetadataMapByName; this.fieldMetadataMapByName = fieldMetadataMapByName;
this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName;
this.featureFlagsMap = featureFlagsMap; this.featureFlagsMap = featureFlagsMap;
} }
@ -35,7 +38,9 @@ export class GraphqlQueryFilterFieldParser {
filterValue: any, filterValue: any,
isFirst = false, isFirst = false,
): void { ): void {
const fieldMetadata = this.fieldMetadataMapByName[`${key}`]; const fieldMetadata =
this.fieldMetadataMapByName[`${key}`] ||
this.fieldMetadataMapByJoinColumnName[`${key}`];
if (!fieldMetadata) { if (!fieldMetadata) {
throw new Error(`Field metadata not found for field: ${key}`); throw new Error(`Field metadata not found for field: ${key}`);

View File

@ -24,6 +24,7 @@ import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modu
export class GraphqlQueryParser { export class GraphqlQueryParser {
private fieldMetadataMapByName: FieldMetadataMap; private fieldMetadataMapByName: FieldMetadataMap;
private fieldMetadataMapByJoinColumnName: FieldMetadataMap;
private objectMetadataMaps: ObjectMetadataMaps; private objectMetadataMaps: ObjectMetadataMaps;
private filterConditionParser: GraphqlQueryFilterConditionParser; private filterConditionParser: GraphqlQueryFilterConditionParser;
private orderFieldParser: GraphqlQueryOrderFieldParser; private orderFieldParser: GraphqlQueryOrderFieldParser;
@ -31,14 +32,17 @@ export class GraphqlQueryParser {
constructor( constructor(
fieldMetadataMapByName: FieldMetadataMap, fieldMetadataMapByName: FieldMetadataMap,
fieldMetadataMapByJoinColumnName: FieldMetadataMap,
objectMetadataMaps: ObjectMetadataMaps, objectMetadataMaps: ObjectMetadataMaps,
featureFlagsMap: FeatureFlagMap, featureFlagsMap: FeatureFlagMap,
) { ) {
this.objectMetadataMaps = objectMetadataMaps; this.objectMetadataMaps = objectMetadataMaps;
this.fieldMetadataMapByName = fieldMetadataMapByName; this.fieldMetadataMapByName = fieldMetadataMapByName;
this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName;
this.featureFlagsMap = featureFlagsMap; this.featureFlagsMap = featureFlagsMap;
this.filterConditionParser = new GraphqlQueryFilterConditionParser( this.filterConditionParser = new GraphqlQueryFilterConditionParser(
this.fieldMetadataMapByName, this.fieldMetadataMapByName,
this.fieldMetadataMapByJoinColumnName,
featureFlagsMap, featureFlagsMap,
); );
this.orderFieldParser = new GraphqlQueryOrderFieldParser( this.orderFieldParser = new GraphqlQueryOrderFieldParser(

View File

@ -23,7 +23,7 @@ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-met
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { isFieldMetadataOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
@Injectable() @Injectable()
export class ProcessNestedRelationsV2Helper { export class ProcessNestedRelationsV2Helper {
@ -103,7 +103,10 @@ export class ProcessNestedRelationsV2Helper {
parentObjectMetadataItem.fieldsByName[sourceFieldName]; parentObjectMetadataItem.fieldsByName[sourceFieldName];
if ( if (
!isFieldMetadataOfType(sourceFieldMetadata, FieldMetadataType.RELATION) !isFieldMetadataInterfaceOfType(
sourceFieldMetadata,
FieldMetadataType.RELATION,
)
) { ) {
// TODO: Maybe we should throw an error here ? // TODO: Maybe we should throw an error here ?
return; return;
@ -149,7 +152,7 @@ export class ProcessNestedRelationsV2Helper {
? `"${targetRelationName}Id"` ? `"${targetRelationName}Id"`
: 'id', : 'id',
ids: relationIds, ids: relationIds,
limit, limit: limit * parentObjectRecords.length,
objectMetadataMaps, objectMetadataMaps,
targetObjectMetadata, targetObjectMetadata,
aggregate, aggregate,
@ -313,6 +316,7 @@ export class ProcessNestedRelationsV2Helper {
result, result,
targetObjectMetadata, targetObjectMetadata,
objectMetadataMaps, objectMetadataMaps,
true,
); );
return { relationResults, relationAggregatedFieldsResult }; return { relationResults, relationAggregatedFieldsResult };

View File

@ -453,6 +453,7 @@ export class ProcessNestedRelationsHelper {
result, result,
referenceObjectMetadata, referenceObjectMetadata,
objectMetadataMaps, objectMetadataMaps,
false,
); );
return { relationResults, relationAggregatedFieldsResult }; return { relationResults, relationAggregatedFieldsResult };

View File

@ -130,6 +130,7 @@ export abstract class GraphqlQueryBaseResolverService<
const graphqlQueryParser = new GraphqlQueryParser( const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps.fieldsByName, objectMetadataItemWithFieldMaps.fieldsByName,
objectMetadataItemWithFieldMaps.fieldsByJoinColumnName,
options.objectMetadataMaps, options.objectMetadataMaps,
featureFlagsMap, featureFlagsMap,
); );

View File

@ -44,6 +44,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
objectRecords, objectRecords,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap,
); );
this.apiEventEmitterService.emitCreateEvents( this.apiEventEmitterService.emitCreateEvents(
@ -301,6 +302,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
objectRecords: InsertResult, objectRecords: InsertResult,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps, objectMetadataMaps: ObjectMetadataMaps,
featureFlagsMap: Record<FeatureFlagKey, boolean>,
): Promise<ObjectRecord[]> { ): Promise<ObjectRecord[]> {
const queryBuilder = executionArgs.repository.createQueryBuilder( const queryBuilder = executionArgs.repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular, objectMetadataItemWithFieldMaps.nameSingular,
@ -317,6 +319,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
nonFormattedUpsertedRecords, nonFormattedUpsertedRecords,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
} }

View File

@ -53,6 +53,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv
nonFormattedUpsertedRecords, nonFormattedUpsertedRecords,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
this.apiEventEmitterService.emitCreateEvents( this.apiEventEmitterService.emitCreateEvents(

View File

@ -54,6 +54,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
nonFormattedDeletedObjectRecords.raw, nonFormattedDeletedObjectRecords.raw,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
this.apiEventEmitterService.emitDeletedEvents( this.apiEventEmitterService.emitDeletedEvents(

View File

@ -47,6 +47,7 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
nonFormattedDeletedObjectRecords.raw, nonFormattedDeletedObjectRecords.raw,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
this.apiEventEmitterService.emitDeletedEvents( this.apiEventEmitterService.emitDeletedEvents(

View File

@ -52,6 +52,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
nonFormattedDeletedObjectRecords.raw, nonFormattedDeletedObjectRecords.raw,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
this.apiEventEmitterService.emitDestroyEvents( this.apiEventEmitterService.emitDestroyEvents(

View File

@ -52,6 +52,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
nonFormattedDeletedObjectRecords.raw, nonFormattedDeletedObjectRecords.raw,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
this.apiEventEmitterService.emitDestroyEvents( this.apiEventEmitterService.emitDestroyEvents(

View File

@ -64,6 +64,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
const graphqlQueryParser = new GraphqlQueryParser( const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldsMaps?.fieldsByName, objectMetadataItemWithFieldsMaps?.fieldsByName,
objectMetadataItemWithFieldsMaps?.fieldsByJoinColumnName,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap, featureFlagsMap,
); );
@ -85,6 +86,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
nonFormattedObjectRecords, nonFormattedObjectRecords,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
} else if (executionArgs.args.data && !isEmpty(executionArgs.args.data)) { } else if (executionArgs.args.data && !isEmpty(executionArgs.args.data)) {
objectRecords = formatData( objectRecords = formatData(
@ -131,6 +133,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
nonFormattedDuplicates, nonFormattedDuplicates,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
return typeORMObjectRecordsParser.createConnection({ return typeORMObjectRecordsParser.createConnection({

View File

@ -130,6 +130,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
nonFormattedObjectRecords, nonFormattedObjectRecords,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
const { hasNextPage, hasPreviousPage } = getPaginationInfo( const { hasNextPage, hasPreviousPage } = getPaginationInfo(

View File

@ -59,6 +59,7 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver
nonFormattedObjectRecord, nonFormattedObjectRecord,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
if (!objectRecord) { if (!objectRecord) {

View File

@ -54,6 +54,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
nonFormattedRestoredObjectRecords.raw, nonFormattedRestoredObjectRecords.raw,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
this.apiEventEmitterService.emitRestoreEvents( this.apiEventEmitterService.emitRestoreEvents(

View File

@ -47,6 +47,7 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
nonFormattedRestoredObjectRecords.raw, nonFormattedRestoredObjectRecords.raw,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
this.apiEventEmitterService.emitRestoreEvents( this.apiEventEmitterService.emitRestoreEvents(

View File

@ -55,6 +55,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
existingRecords, existingRecords,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
if (isEmpty(formattedExistingRecords)) { if (isEmpty(formattedExistingRecords)) {
@ -89,6 +90,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
nonFormattedUpdatedObjectRecords.raw, nonFormattedUpdatedObjectRecords.raw,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
this.apiEventEmitterService.emitUpdateEvents( this.apiEventEmitterService.emitUpdateEvents(

View File

@ -55,6 +55,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
existingRecords, existingRecords,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
if (isEmpty(formattedExistingRecords)) { if (isEmpty(formattedExistingRecords)) {
@ -74,6 +75,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
nonFormattedUpdatedObjectRecords.raw, nonFormattedUpdatedObjectRecords.raw,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
); );
this.apiEventEmitterService.emitUpdateEvents( this.apiEventEmitterService.emitUpdateEvents(

View File

@ -22,7 +22,7 @@ import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/work
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { isFieldMetadataOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
// TODO: find a way to prevent conflict between handlers executing logic on object relations // TODO: find a way to prevent conflict between handlers executing logic on object relations
// And this factory that is also executing logic on object relations // And this factory that is also executing logic on object relations
@ -153,7 +153,10 @@ export class QueryResultGettersFactory {
) )
.filter(isDefined) .filter(isDefined)
.filter((fieldMetadata) => .filter((fieldMetadata) =>
isFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION), isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
),
); );
const relationFieldsProcessedMap = {} as Record< const relationFieldsProcessedMap = {} as Record<

View File

@ -15,7 +15,7 @@ import { RelationTypeV2Factory } from 'src/engine/api/graphql/workspace-schema-b
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage'; import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util'; import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field'; import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field';
import { isFieldMetadataOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { ArgsFactory } from './args.factory'; import { ArgsFactory } from './args.factory';
@ -108,7 +108,12 @@ export class ExtendObjectTypeDefinitionV2Factory {
for (const fieldMetadata of objectMetadata.fields) { for (const fieldMetadata of objectMetadata.fields) {
// Ignore non-relation fields as they are already defined // Ignore non-relation fields as they are already defined
if (!isFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION)) { if (
!isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
)
) {
continue; continue;
} }

View File

@ -36,6 +36,7 @@ export class InputTypeDefinitionFactory {
objectMetadata: ObjectMetadataInterface, objectMetadata: ObjectMetadataInterface,
kind: InputTypeDefinitionKind, kind: InputTypeDefinitionKind,
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
isNewRelationEnabled = false,
): InputTypeDefinition { ): InputTypeDefinition {
const inputType = new GraphQLInputObjectType({ const inputType = new GraphQLInputObjectType({
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`, name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`,
@ -58,6 +59,7 @@ export class InputTypeDefinitionFactory {
kind, kind,
options, options,
this.inputTypeFactory, this.inputTypeFactory,
isNewRelationEnabled,
), ),
and: { and: {
type: andOrType, type: andOrType,
@ -81,6 +83,7 @@ export class InputTypeDefinitionFactory {
kind, kind,
options, options,
this.inputTypeFactory, this.inputTypeFactory,
isNewRelationEnabled,
); );
} }
}, },

View File

@ -30,6 +30,7 @@ export class ObjectTypeDefinitionFactory {
objectMetadata: ObjectMetadataInterface, objectMetadata: ObjectMetadataInterface,
kind: ObjectTypeDefinitionKind, kind: ObjectTypeDefinitionKind,
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
isNewRelationEnabled = false,
): ObjectTypeDefinition { ): ObjectTypeDefinition {
return { return {
target: objectMetadata.id, target: objectMetadata.id,
@ -42,6 +43,7 @@ export class ObjectTypeDefinitionFactory {
kind, kind,
options, options,
this.outputTypeFactory, this.outputTypeFactory,
isNewRelationEnabled,
), ),
}), }),
}; };

View File

@ -58,7 +58,7 @@ export class TypeMapperService {
settings?: FieldMetadataSettings<FieldMetadataType>, settings?: FieldMetadataSettings<FieldMetadataType>,
isIdField?: boolean, isIdField?: boolean,
): GraphQLScalarType | undefined { ): GraphQLScalarType | undefined {
if (isIdField || settings?.isForeignKey) { if (isIdField || fieldMetadataType === FieldMetadataType.RELATION) {
return GraphQLID; return GraphQLID;
} }
const typeScalarMapping = new Map<FieldMetadataType, GraphQLScalarType>([ const typeScalarMapping = new Map<FieldMetadataType, GraphQLScalarType>([
@ -93,7 +93,7 @@ export class TypeMapperService {
settings?: FieldMetadataSettings<FieldMetadataType>, settings?: FieldMetadataSettings<FieldMetadataType>,
isIdField?: boolean, isIdField?: boolean,
): GraphQLInputObjectType | GraphQLScalarType | undefined { ): GraphQLInputObjectType | GraphQLScalarType | undefined {
if (isIdField || settings?.isForeignKey) { if (isIdField || fieldMetadataType === FieldMetadataType.RELATION) {
return IDFilterType; return IDFilterType;
} }
@ -132,6 +132,7 @@ export class TypeMapperService {
): GraphQLInputType | undefined { ): GraphQLInputType | undefined {
const typeOrderByMapping = new Map<FieldMetadataType, GraphQLEnumType>([ const typeOrderByMapping = new Map<FieldMetadataType, GraphQLEnumType>([
[FieldMetadataType.UUID, OrderByDirectionType], [FieldMetadataType.UUID, OrderByDirectionType],
[FieldMetadataType.RELATION, OrderByDirectionType],
[FieldMetadataType.TEXT, OrderByDirectionType], [FieldMetadataType.TEXT, OrderByDirectionType],
[FieldMetadataType.DATE_TIME, OrderByDirectionType], [FieldMetadataType.DATE_TIME, OrderByDirectionType],
[FieldMetadataType.DATE, OrderByDirectionType], [FieldMetadataType.DATE, OrderByDirectionType],

View File

@ -51,11 +51,16 @@ export class TypeDefinitionsGenerator {
async generate( async generate(
objectMetadataCollection: ObjectMetadataInterface[], objectMetadataCollection: ObjectMetadataInterface[],
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
isNewRelationEnabled: boolean,
) { ) {
// Generate composite type objects first because they can be used in dynamic objects // Generate composite type objects first because they can be used in dynamic objects
await this.generateCompositeTypeDefs(options); await this.generateCompositeTypeDefs(options);
// Generate metadata objects // Generate metadata objects
await this.generateMetadataTypeDefs(objectMetadataCollection, options); await this.generateMetadataTypeDefs(
objectMetadataCollection,
options,
isNewRelationEnabled,
);
} }
/** /**
@ -155,6 +160,7 @@ export class TypeDefinitionsGenerator {
private async generateMetadataTypeDefs( private async generateMetadataTypeDefs(
dynamicObjectMetadataCollection: ObjectMetadataInterface[], dynamicObjectMetadataCollection: ObjectMetadataInterface[],
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
isNewRelationEnabled: boolean,
) { ) {
this.logger.log( this.logger.log(
`Generating metadata objects: [${dynamicObjectMetadataCollection `Generating metadata objects: [${dynamicObjectMetadataCollection
@ -164,9 +170,17 @@ export class TypeDefinitionsGenerator {
// Generate dynamic objects // Generate dynamic objects
this.generateEnumTypeDefs(dynamicObjectMetadataCollection, options); this.generateEnumTypeDefs(dynamicObjectMetadataCollection, options);
this.generateObjectTypeDefs(dynamicObjectMetadataCollection, options); this.generateObjectTypeDefs(
dynamicObjectMetadataCollection,
options,
isNewRelationEnabled,
);
this.generatePaginationTypeDefs(dynamicObjectMetadataCollection, options); this.generatePaginationTypeDefs(dynamicObjectMetadataCollection, options);
this.generateInputTypeDefs(dynamicObjectMetadataCollection, options); this.generateInputTypeDefs(
dynamicObjectMetadataCollection,
options,
isNewRelationEnabled,
);
await this.generateExtendedObjectTypeDefs( await this.generateExtendedObjectTypeDefs(
dynamicObjectMetadataCollection, dynamicObjectMetadataCollection,
options, options,
@ -176,12 +190,14 @@ export class TypeDefinitionsGenerator {
private generateObjectTypeDefs( private generateObjectTypeDefs(
objectMetadataCollection: ObjectMetadataInterface[] | CompositeType[], objectMetadataCollection: ObjectMetadataInterface[] | CompositeType[],
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
isNewRelationEnabled: boolean,
) { ) {
const objectTypeDefs = objectMetadataCollection.map((objectMetadata) => const objectTypeDefs = objectMetadataCollection.map((objectMetadata) =>
this.objectTypeDefinitionFactory.create( this.objectTypeDefinitionFactory.create(
objectMetadata, objectMetadata,
ObjectTypeDefinitionKind.Plain, ObjectTypeDefinitionKind.Plain,
options, options,
isNewRelationEnabled,
), ),
); );
@ -209,6 +225,7 @@ export class TypeDefinitionsGenerator {
private generateInputTypeDefs( private generateInputTypeDefs(
objectMetadataCollection: ObjectMetadataInterface[], objectMetadataCollection: ObjectMetadataInterface[],
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
isNewRelationEnabled: boolean,
) { ) {
const inputTypeDefs = objectMetadataCollection const inputTypeDefs = objectMetadataCollection
.map((objectMetadata) => { .map((objectMetadata) => {
@ -226,24 +243,28 @@ export class TypeDefinitionsGenerator {
objectMetadata, objectMetadata,
InputTypeDefinitionKind.Create, InputTypeDefinitionKind.Create,
options, options,
isNewRelationEnabled,
), ),
// Input type for update // Input type for update
this.inputTypeDefinitionFactory.create( this.inputTypeDefinitionFactory.create(
optionalExtendedObjectMetadata, optionalExtendedObjectMetadata,
InputTypeDefinitionKind.Update, InputTypeDefinitionKind.Update,
options, options,
isNewRelationEnabled,
), ),
// Filter input type // Filter input type
this.inputTypeDefinitionFactory.create( this.inputTypeDefinitionFactory.create(
optionalExtendedObjectMetadata, optionalExtendedObjectMetadata,
InputTypeDefinitionKind.Filter, InputTypeDefinitionKind.Filter,
options, options,
isNewRelationEnabled,
), ),
// OrderBy input type // OrderBy input type
this.inputTypeDefinitionFactory.create( this.inputTypeDefinitionFactory.create(
optionalExtendedObjectMetadata, optionalExtendedObjectMetadata,
InputTypeDefinitionKind.OrderBy, InputTypeDefinitionKind.OrderBy,
options, options,
isNewRelationEnabled,
), ),
]; ];
}) })

View File

@ -8,11 +8,12 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory'; import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory'; import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
type TypeFactory<T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind> = type TypeFactory<T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind> =
{ {
@ -40,13 +41,21 @@ export const generateFields = <
kind: T, kind: T,
options: WorkspaceBuildSchemaOptions, options: WorkspaceBuildSchemaOptions,
typeFactory: TypeFactory<T>, typeFactory: TypeFactory<T>,
isNewRelationEnabled = false,
): T extends InputTypeDefinitionKind ): T extends InputTypeDefinitionKind
? GraphQLInputFieldConfigMap ? GraphQLInputFieldConfigMap
: GraphQLFieldConfigMap<any, any> => { : GraphQLFieldConfigMap<any, any> => {
const fields = {}; const fields = {};
for (const fieldMetadata of objectMetadata.fields) { for (const fieldMetadata of objectMetadata.fields) {
if (isRelationFieldMetadataType(fieldMetadata.type)) { if (
isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
) &&
(fieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE ||
!isNewRelationEnabled)
) {
continue; continue;
} }
@ -80,6 +89,25 @@ export const generateFields = <
typeFactoryOptions, typeFactoryOptions,
); );
if (
isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
) &&
fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE
) {
const joinColumnName = fieldMetadata.settings?.joinColumnName;
if (!joinColumnName) {
throw new Error('Join column name is not defined');
}
fields[joinColumnName] = {
type,
description: fieldMetadata.description,
};
}
fields[fieldMetadata.name] = { fields[fieldMetadata.name] = {
type, type,
description: fieldMetadata.description, description: fieldMetadata.description,

View File

@ -25,11 +25,13 @@ export class WorkspaceGraphQLSchemaFactory {
objectMetadataCollection: ObjectMetadataInterface[], objectMetadataCollection: ObjectMetadataInterface[],
workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods, workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods,
options: WorkspaceBuildSchemaOptions = {}, options: WorkspaceBuildSchemaOptions = {},
isNewRelationEnabled = false,
): Promise<GraphQLSchema> { ): Promise<GraphQLSchema> {
// Generate type definitions // Generate type definitions
await this.typeDefinitionsGenerator.generate( await this.typeDefinitionsGenerator.generate(
objectMetadataCollection, objectMetadataCollection,
options, options,
isNewRelationEnabled,
); );
// Generate schema // Generate schema

View File

@ -149,6 +149,8 @@ export class WorkspaceSchemaFactory {
await this.workspaceGraphQLSchemaFactory.create( await this.workspaceGraphQLSchemaFactory.create(
objectMetadataCollection, objectMetadataCollection,
workspaceResolverBuilderMethodNames, workspaceResolverBuilderMethodNames,
{},
isNewRelationEnabled,
); );
usedScalarNames = usedScalarNames =

View File

@ -31,6 +31,7 @@ describe('checkFields', () => {
...objectMetadataItemMock, ...objectMetadataItemMock,
fieldsById, fieldsById,
fieldsByName, fieldsByName,
fieldsByJoinColumnName: {},
}; };
it('should check field types', () => { it('should check field types', () => {

View File

@ -32,6 +32,7 @@ describe('getFieldType', () => {
...objectMetadataItemMock, ...objectMetadataItemMock,
fieldsById, fieldsById,
fieldsByName, fieldsByName,
fieldsByJoinColumnName: {},
}; };
it('should get field type', () => { it('should get field type', () => {

View File

@ -61,6 +61,7 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
...objectMetadataItemMock, ...objectMetadataItemMock,
fieldsById, fieldsById,
fieldsByName, fieldsByName,
fieldsByJoinColumnName: {},
}; };
const objectMetadataMapsMock: ObjectMetadataMaps = { const objectMetadataMapsMock: ObjectMetadataMaps = {

View File

@ -33,6 +33,7 @@ describe('checkFilterEnumValues', () => {
...objectMetadataItemMock, ...objectMetadataItemMock,
fieldsById, fieldsById,
fieldsByName, fieldsByName,
fieldsByJoinColumnName: {},
}; };
it('should check properly', () => { it('should check properly', () => {

View File

@ -43,6 +43,7 @@ describe('parseFilter', () => {
...objectMetadataItemMock, ...objectMetadataItemMock,
fieldsById, fieldsById,
fieldsByName, fieldsByName,
fieldsByJoinColumnName: {},
}; };
it('should parse string filter test 1', () => { it('should parse string filter test 1', () => {

View File

@ -72,6 +72,7 @@ export class AccessTokenService {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>( await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId, workspaceId,
'workspaceMember', 'workspaceMember',
false,
); );
const workspaceMember = await workspaceMemberRepository.findOne({ const workspaceMember = await workspaceMemberRepository.findOne({

View File

@ -62,6 +62,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
workspaceId: '', workspaceId: '',
}, },
}, },
fieldsByJoinColumnName: {},
}, },
{ {
id: '', id: '',
@ -140,6 +141,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
workspaceId: '', workspaceId: '',
}, },
}, },
fieldsByJoinColumnName: {},
}, },
{ {
id: '', id: '',
@ -218,6 +220,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
workspaceId: '', workspaceId: '',
}, },
}, },
fieldsByJoinColumnName: {},
}, },
{ {
id: '', id: '',
@ -243,5 +246,6 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
indexMetadatas: [], indexMetadatas: [],
fieldsById: {}, fieldsById: {},
fieldsByName: {}, fieldsByName: {},
fieldsByJoinColumnName: {},
}, },
]; ];

View File

@ -74,6 +74,7 @@ export class SearchService {
const queryParser = new GraphqlQueryParser( const queryParser = new GraphqlQueryParser(
objectMetadataItem.fieldsByName, objectMetadataItem.fieldsByName,
objectMetadataItem.fieldsByJoinColumnName,
generateObjectMetadataMaps([objectMetadataItem]), generateObjectMetadataMaps([objectMetadataItem]),
featureFlagMap, featureFlagMap,
); );

View File

@ -25,7 +25,10 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { CreateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { RelationDefinitionDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation-definition.dto'; import {
RelationDefinitionDTO,
RelationDefinitionType,
} from 'src/engine/metadata-modules/field-metadata/dtos/relation-definition.dto';
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto'; import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
import { import {
UpdateFieldInput, UpdateFieldInput,
@ -42,6 +45,7 @@ import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-mod
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { createDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
@UseGuards(WorkspaceAuthGuard) @UseGuards(WorkspaceAuthGuard)
@Resolver(() => FieldMetadataDTO) @Resolver(() => FieldMetadataDTO)
@ -180,6 +184,37 @@ export class FieldMetadataResolver {
return null; return null;
} }
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspace.id,
);
// TODO: Remove this once we drop old relations or update the front-end to use the new relation
if (isNewRelationEnabled) {
const relation = await this.relation(
workspace,
fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>,
context,
);
if (!relation) {
return null;
}
return {
// Temporary fix as we don't have relationId in the new relation
relationId: createDeterministicUuid([
relation.sourceFieldMetadata.id,
relation.targetFieldMetadata.id,
]),
direction: relation.type as unknown as RelationDefinitionType,
sourceObjectMetadata: relation.sourceObjectMetadata,
targetObjectMetadata: relation.targetObjectMetadata,
sourceFieldMetadata: relation.sourceFieldMetadata,
targetFieldMetadata: relation.targetFieldMetadata,
};
}
try { try {
const relationMetadataItem = const relationMetadataItem =
await context.loaders.relationMetadataLoader.load({ await context.loaders.relationMetadataLoader.load({

View File

@ -42,7 +42,7 @@ export type FieldMetadataDateTimeSettings = {
export type FieldMetadataRelationSettings = { export type FieldMetadataRelationSettings = {
relationType: RelationType; relationType: RelationType;
onDelete?: RelationOnDeleteAction; onDelete?: RelationOnDeleteAction;
joinColumnName?: string; joinColumnName?: string | null;
}; };
type FieldMetadataSettingsMapping = { type FieldMetadataSettingsMapping = {

View File

@ -19,6 +19,7 @@ import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor'; import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor';
import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver'; import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver';
import { ObjectMetadataFieldRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service';
import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service'; import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service';
import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service'; import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service';
import { ObjectMetadataRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service'; import { ObjectMetadataRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service';
@ -63,6 +64,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input';
ObjectMetadataService, ObjectMetadataService,
ObjectMetadataMigrationService, ObjectMetadataMigrationService,
ObjectMetadataRelationService, ObjectMetadataRelationService,
ObjectMetadataFieldRelationService,
ObjectMetadataRelatedRecordsService, ObjectMetadataRelatedRecordsService,
], ],
resolvers: [ resolvers: [

View File

@ -10,6 +10,8 @@ import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm';
import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map'; import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -24,6 +26,7 @@ import {
ObjectMetadataException, ObjectMetadataException,
ObjectMetadataExceptionCode, ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { ObjectMetadataFieldRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service';
import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service'; import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service';
import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service'; import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service';
import { ObjectMetadataRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service'; import { ObjectMetadataRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service';
@ -61,9 +64,11 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly searchVectorService: SearchVectorService, private readonly searchVectorService: SearchVectorService,
private readonly objectMetadataRelationService: ObjectMetadataRelationService, private readonly objectMetadataRelationService: ObjectMetadataRelationService,
private readonly objectMetadataFieldRelationService: ObjectMetadataFieldRelationService,
private readonly objectMetadataMigrationService: ObjectMetadataMigrationService, private readonly objectMetadataMigrationService: ObjectMetadataMigrationService,
private readonly objectMetadataRelatedRecordsService: ObjectMetadataRelatedRecordsService, private readonly objectMetadataRelatedRecordsService: ObjectMetadataRelatedRecordsService,
private readonly indexMetadataService: IndexMetadataService, private readonly indexMetadataService: IndexMetadataService,
private readonly featureFlagService: FeatureFlagService,
) { ) {
super(objectMetadataRepository); super(objectMetadataRepository);
} }
@ -181,17 +186,33 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
createdObjectMetadata.fields, createdObjectMetadata.fields,
); );
const createdRelatedObjectMetadataCollection = const isNewRelationEnabled =
await this.objectMetadataRelationService.createRelationsAndForeignKeysMetadata( await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
objectMetadataInput.workspaceId, objectMetadataInput.workspaceId,
createdObjectMetadata,
{
primaryKeyFieldMetadataSettings:
objectMetadataInput.primaryKeyFieldMetadataSettings,
primaryKeyColumnType: objectMetadataInput.primaryKeyColumnType,
},
); );
let createdRelatedObjectMetadataCollection: ObjectMetadataEntity[];
if (isNewRelationEnabled) {
createdRelatedObjectMetadataCollection =
await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata(
objectMetadataInput.workspaceId,
createdObjectMetadata,
);
} else {
createdRelatedObjectMetadataCollection =
await this.objectMetadataRelationService.createRelationsAndForeignKeysMetadata(
objectMetadataInput.workspaceId,
createdObjectMetadata,
{
primaryKeyFieldMetadataSettings:
objectMetadataInput.primaryKeyFieldMetadataSettings,
primaryKeyColumnType: objectMetadataInput.primaryKeyColumnType,
},
);
}
await this.objectMetadataMigrationService.createRelationMigrations( await this.objectMetadataMigrationService.createRelationMigrations(
createdObjectMetadata, createdObjectMetadata,
createdRelatedObjectMetadataCollection, createdRelatedObjectMetadataCollection,
@ -295,11 +316,19 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
inputPayload, inputPayload,
); );
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
if (inputPayload.isActive !== undefined) { if (inputPayload.isActive !== undefined) {
await this.objectMetadataRelationService.updateObjectRelationshipsActivationStatus( // For new relation system, the active status is stitched to the field metadata
inputId, if (!isNewRelationEnabled) {
inputPayload.isActive, await this.objectMetadataRelationService.updateObjectRelationshipsActivationStatus(
); inputId,
inputPayload.isActive,
);
}
} }
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
@ -342,8 +371,17 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
input: DeleteOneObjectInput, input: DeleteOneObjectInput,
workspaceId: string, workspaceId: string,
): Promise<ObjectMetadataEntity> { ): Promise<ObjectMetadataEntity> {
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
const objectMetadata = await this.objectMetadataRepository.findOne({ const objectMetadata = await this.objectMetadataRepository.findOne({
relations: [ relations: [
'fields',
'fields.object',
'fields.relationTargetFieldMetadata',
'fields.relationTargetFieldMetadata.object',
'fromRelations.fromFieldMetadata', 'fromRelations.fromFieldMetadata',
'fromRelations.toFieldMetadata', 'fromRelations.toFieldMetadata',
'toRelations.fromFieldMetadata', 'toRelations.fromFieldMetadata',
@ -375,6 +413,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
await this.objectMetadataMigrationService.deleteAllRelationsAndDropTable( await this.objectMetadataMigrationService.deleteAllRelationsAndDropTable(
objectMetadata, objectMetadata,
workspaceId, workspaceId,
isNewRelationEnabled,
); );
} }
@ -387,6 +426,15 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId, workspaceId,
); );
const fieldMetadataIds = objectMetadata.fields.map((field) => field.id);
const relationMetadataIds = objectMetadata.fields
.map((field) => field.relationTargetFieldMetadata?.id)
.filter(isDefined);
await this.fieldMetadataRepository.delete({
id: In(fieldMetadataIds.concat(relationMetadataIds)),
});
await this.objectMetadataRepository.delete(objectMetadata.id); await this.objectMetadataRepository.delete(objectMetadata.id);
await this.workspaceMetadataVersionService.incrementMetadataVersion( await this.workspaceMetadataVersionService.incrementMetadataVersion(
@ -478,6 +526,11 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadataForUpdate: ObjectMetadataEntity, objectMetadataForUpdate: ObjectMetadataEntity,
inputPayload: UpdateObjectPayload, inputPayload: UpdateObjectPayload,
) { ) {
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
objectMetadataForUpdate.workspaceId,
);
const newTargetTableName = computeObjectTargetTable( const newTargetTableName = computeObjectTargetTable(
objectMetadataForUpdate, objectMetadataForUpdate,
); );
@ -492,18 +545,33 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadataForUpdate.workspaceId, objectMetadataForUpdate.workspaceId,
); );
const relationsAndForeignKeysMetadata = if (isNewRelationEnabled) {
await this.objectMetadataRelationService.updateRelationsAndForeignKeysMetadata( const relationMetadataCollection =
objectMetadataForUpdate.workspaceId, await this.objectMetadataFieldRelationService.updateRelationsAndForeignKeysMetadata(
objectMetadataForUpdate, objectMetadataForUpdate.workspaceId,
); objectMetadataForUpdate,
);
await this.objectMetadataMigrationService.createUpdateForeignKeysMigrations( await this.objectMetadataMigrationService.updateRelationMigrations(
existingObjectMetadata, existingObjectMetadata,
objectMetadataForUpdate, objectMetadataForUpdate,
relationsAndForeignKeysMetadata, relationMetadataCollection,
objectMetadataForUpdate.workspaceId, objectMetadataForUpdate.workspaceId,
); );
} else {
const relationsAndForeignKeysMetadata =
await this.objectMetadataRelationService.updateRelationsAndForeignKeysMetadata(
objectMetadataForUpdate.workspaceId,
objectMetadataForUpdate,
);
await this.objectMetadataMigrationService.createUpdateForeignKeysMigrations(
existingObjectMetadata,
objectMetadataForUpdate,
relationsAndForeignKeysMetadata,
objectMetadataForUpdate.workspaceId,
);
}
await this.objectMetadataMigrationService.recomputeEnumNames( await this.objectMetadataMigrationService.recomputeEnumNames(
objectMetadataForUpdate, objectMetadataForUpdate,

View File

@ -0,0 +1,311 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FieldMetadataType } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 as uuidV4 } from 'uuid';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util';
import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import {
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
STANDARD_OBJECT_FIELD_IDS,
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { createRelationDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [
STANDARD_OBJECT_IDS.timelineActivity,
STANDARD_OBJECT_IDS.favorite,
STANDARD_OBJECT_IDS.attachment,
STANDARD_OBJECT_IDS.noteTarget,
STANDARD_OBJECT_IDS.taskTarget,
];
@Injectable()
export class ObjectMetadataFieldRelationService {
constructor(
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
) {}
public async createRelationsAndForeignKeysMetadata(
workspaceId: string,
sourceObjectMetadata: ObjectMetadataEntity,
) {
const relatedObjectMetadataCollection = await Promise.all(
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map(
async (relationObjectMetadataStandardId) =>
this.createRelationAndForeignKeyMetadata(
workspaceId,
sourceObjectMetadata,
relationObjectMetadataStandardId,
),
),
);
return relatedObjectMetadataCollection;
}
private async createRelationAndForeignKeyMetadata(
workspaceId: string,
sourceObjectMetadata: ObjectMetadataEntity,
relationObjectMetadataStandardId: string,
) {
const targetObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
standardId: relationObjectMetadataStandardId,
workspaceId: workspaceId,
isCustom: false,
});
await this.createFieldMetadataRelation(
workspaceId,
sourceObjectMetadata,
targetObjectMetadata,
);
return targetObjectMetadata;
}
private async createFieldMetadataRelation(
workspaceId: string,
sourceObjectMetadata: ObjectMetadataEntity,
targetObjectMetadata: ObjectMetadataEntity,
): Promise<FieldMetadataEntity<FieldMetadataType.RELATION>[]> {
const sourceFieldMetadata = this.createSourceFieldMetadata(
workspaceId,
sourceObjectMetadata,
targetObjectMetadata,
);
const targetFieldMetadata = this.createTargetFieldMetadata(
workspaceId,
sourceObjectMetadata,
targetObjectMetadata,
);
return this.fieldMetadataRepository.save([
{
...sourceFieldMetadata,
settings: {
relationType: RelationType.ONE_TO_MANY,
onDelete: RelationOnDeleteAction.CASCADE,
},
relationTargetObjectMetadataId: targetObjectMetadata.id,
relationTargetFieldMetadataId: targetFieldMetadata.id,
} as Partial<FieldMetadataEntity<FieldMetadataType.RELATION>>,
{
...targetFieldMetadata,
settings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.CASCADE,
joinColumnName: `${sourceObjectMetadata.nameSingular}Id`,
},
relationTargetObjectMetadataId: sourceObjectMetadata.id,
relationTargetFieldMetadataId: sourceFieldMetadata.id,
} as Partial<FieldMetadataEntity<FieldMetadataType.RELATION>>,
]);
}
public async updateRelationsAndForeignKeysMetadata(
workspaceId: string,
updatedObjectMetadata: ObjectMetadataEntity,
): Promise<
{
targetObjectMetadata: ObjectMetadataEntity;
targetFieldMetadata: FieldMetadataEntity;
sourceFieldMetadata: FieldMetadataEntity;
}[]
> {
return await Promise.all(
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map(
async (relationObjectMetadataStandardId) =>
this.updateRelationAndForeignKeyMetadata(
workspaceId,
updatedObjectMetadata,
relationObjectMetadataStandardId,
),
),
);
}
private async updateRelationAndForeignKeyMetadata(
workspaceId: string,
sourceObjectMetadata: ObjectMetadataEntity,
targetObjectMetadataStandardId: string,
) {
const targetObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
standardId: targetObjectMetadataStandardId,
workspaceId: workspaceId,
isCustom: false,
});
const targetFieldMetadataUpdateData = this.updateTargetFieldMetadata(
sourceObjectMetadata,
targetObjectMetadata,
);
const targetFieldMetadataToUpdate =
await this.fieldMetadataRepository.findOneByOrFail({
standardId: createRelationDeterministicUuid({
objectId: sourceObjectMetadata.id,
standardId:
STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom,
}),
objectMetadataId: targetObjectMetadata.id,
workspaceId: workspaceId,
});
const targetFieldMetadata = await this.fieldMetadataRepository.save({
id: targetFieldMetadataToUpdate.id,
...targetFieldMetadataUpdateData,
});
const sourceFieldMetadataUpdateData = this.updateSourceFieldMetadata(
sourceObjectMetadata,
targetObjectMetadata,
);
const sourceFieldMetadataToUpdate =
await this.fieldMetadataRepository.findOneByOrFail({
standardId:
CUSTOM_OBJECT_STANDARD_FIELD_IDS[targetObjectMetadata.namePlural],
objectMetadataId: sourceObjectMetadata.id,
workspaceId: workspaceId,
});
const sourceFieldMetadata = await this.fieldMetadataRepository.save({
id: sourceFieldMetadataToUpdate.id,
...sourceFieldMetadataUpdateData,
});
return {
targetObjectMetadata,
targetFieldMetadata,
sourceFieldMetadata,
};
}
private createSourceFieldMetadata(
workspaceId: string,
sourceObjectMetadata: ObjectMetadataEntity,
targetObjectMetadata: ObjectMetadataEntity,
): Partial<FieldMetadataEntity<FieldMetadataType.RELATION>> {
const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural;
const { description } = buildDescriptionForRelationFieldMetadataOnFromField(
{
relationObjectMetadataNamePlural,
targetObjectLabelSingular: sourceObjectMetadata.labelSingular,
},
);
return {
id: uuidV4(),
standardId:
CUSTOM_OBJECT_STANDARD_FIELD_IDS[relationObjectMetadataNamePlural],
objectMetadataId: sourceObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
isSystem: true,
type: FieldMetadataType.RELATION,
name: targetObjectMetadata.namePlural,
label: capitalize(relationObjectMetadataNamePlural),
description,
icon:
STANDARD_OBJECT_ICONS[targetObjectMetadata.nameSingular] ||
'IconBuildingSkyscraper',
isNullable: true,
};
}
private updateSourceFieldMetadata(
sourceObjectMetadata: ObjectMetadataEntity,
targetObjectMetadata: ObjectMetadataEntity,
) {
const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural;
const { description } = buildDescriptionForRelationFieldMetadataOnFromField(
{
relationObjectMetadataNamePlural,
targetObjectLabelSingular: sourceObjectMetadata.labelSingular,
},
);
return {
description,
};
}
private createTargetFieldMetadata(
workspaceId: string,
sourceObjectMetadata: ObjectMetadataEntity,
targetObjectMetadata: ObjectMetadataEntity,
): Partial<FieldMetadataEntity<FieldMetadataType.RELATION>> {
const customStandardFieldId =
STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom;
if (!customStandardFieldId) {
throw new Error(
`Custom standard field ID not found for ${targetObjectMetadata.nameSingular}`,
);
}
const { description } = buildDescriptionForRelationFieldMetadataOnToField({
relationObjectMetadataNamePlural: targetObjectMetadata.namePlural,
targetObjectLabelSingular: sourceObjectMetadata.labelSingular,
});
return {
id: uuidV4(),
name: sourceObjectMetadata.nameSingular,
label: sourceObjectMetadata.labelSingular,
description,
standardId: createRelationDeterministicUuid({
objectId: sourceObjectMetadata.id,
standardId: customStandardFieldId,
}),
objectMetadataId: targetObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
isSystem: true,
type: FieldMetadataType.RELATION,
icon: 'IconBuildingSkyscraper',
isNullable: true,
};
}
private updateTargetFieldMetadata(
sourceObjectMetadata: ObjectMetadataEntity,
targetObjectMetadata: ObjectMetadataEntity,
) {
const customStandardFieldId =
STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom;
if (!customStandardFieldId) {
throw new Error(
`Custom standard field ID not found for ${targetObjectMetadata.nameSingular}`,
);
}
const { description } = buildDescriptionForRelationFieldMetadataOnToField({
relationObjectMetadataNamePlural: targetObjectMetadata.namePlural,
targetObjectLabelSingular: sourceObjectMetadata.labelSingular,
});
return {
name: sourceObjectMetadata.nameSingular,
label: sourceObjectMetadata.labelSingular,
description,
};
}
}

View File

@ -1,8 +1,10 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { In, Repository } from 'typeorm';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
@ -22,6 +24,7 @@ import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util'; import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { RELATION_MIGRATION_PRIORITY_PREFIX } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { RELATION_MIGRATION_PRIORITY_PREFIX } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
@Injectable() @Injectable()
@ -117,6 +120,52 @@ export class ObjectMetadataMigrationService {
); );
} }
public async updateRelationMigrations(
currentObjectMetadata: ObjectMetadataEntity,
alteredObjectMetadata: ObjectMetadataEntity,
relationMetadataCollection: {
targetObjectMetadata: ObjectMetadataEntity;
targetFieldMetadata: FieldMetadataEntity;
sourceFieldMetadata: FieldMetadataEntity;
}[],
workspaceId: string,
) {
for (const { targetObjectMetadata } of relationMetadataCollection) {
const targetTableName = computeObjectTargetTable(targetObjectMetadata);
const columnName = `${currentObjectMetadata.nameSingular}Id`;
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`rename-${currentObjectMetadata.nameSingular}-to-${alteredObjectMetadata.nameSingular}-in-${targetObjectMetadata.nameSingular}`,
),
workspaceId,
[
{
name: targetTableName,
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.ALTER,
currentColumnDefinition: {
columnName,
columnType: 'uuid',
isNullable: true,
defaultValue: null,
},
alteredColumnDefinition: {
columnName: `${alteredObjectMetadata.nameSingular}Id`,
columnType: 'uuid',
isNullable: true,
defaultValue: null,
},
},
],
},
],
);
}
}
public async createUpdateForeignKeysMigrations( public async createUpdateForeignKeysMigrations(
existingObjectMetadata: ObjectMetadataEntity, existingObjectMetadata: ObjectMetadataEntity,
updatedObjectMetadata: ObjectMetadataEntity, updatedObjectMetadata: ObjectMetadataEntity,
@ -173,15 +222,16 @@ export class ObjectMetadataMigrationService {
public async deleteAllRelationsAndDropTable( public async deleteAllRelationsAndDropTable(
objectMetadata: ObjectMetadataEntity, objectMetadata: ObjectMetadataEntity,
workspaceId: string, workspaceId: string,
isNewRelationEnabled: boolean,
) { ) {
const relationsToDelete: RelationToDelete[] = []; const relationsMetadataToDelete: RelationToDelete[] = [];
// TODO: Most of this logic should be moved to relation-metadata.service.ts // TODO: Most of this logic should be moved to relation-metadata.service.ts
for (const relation of [ for (const relation of [
...objectMetadata.fromRelations, ...objectMetadata.fromRelations,
...objectMetadata.toRelations, ...objectMetadata.toRelations,
]) { ]) {
relationsToDelete.push({ relationsMetadataToDelete.push({
id: relation.id, id: relation.id,
fromFieldMetadataId: relation.fromFieldMetadata.id, fromFieldMetadataId: relation.fromFieldMetadata.id,
toFieldMetadataId: relation.toFieldMetadata.id, toFieldMetadataId: relation.toFieldMetadata.id,
@ -201,13 +251,13 @@ export class ObjectMetadataMigrationService {
}); });
} }
if (relationsToDelete.length > 0) { if (relationsMetadataToDelete.length > 0) {
await this.relationMetadataRepository.delete( await this.relationMetadataRepository.delete(
relationsToDelete.map((relation) => relation.id), relationsMetadataToDelete.map((relation) => relation.id),
); );
} }
for (const relationToDelete of relationsToDelete) { for (const relationToDelete of relationsMetadataToDelete) {
const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({ const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({
where: { where: {
name: `${relationToDelete.toFieldMetadataName}Id`, name: `${relationToDelete.toFieldMetadataName}Id`,
@ -254,6 +304,61 @@ export class ObjectMetadataMigrationService {
} }
} }
if (isNewRelationEnabled) {
const manyToOneRelationFieldsToDelete = objectMetadata.fields.filter(
(field) =>
isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION) &&
(field as FieldMetadataEntity<FieldMetadataType.RELATION>).settings
?.relationType === RelationType.MANY_TO_ONE,
) as FieldMetadataEntity<FieldMetadataType.RELATION>[];
const oneToManyRelationFieldsToDelete = objectMetadata.fields.filter(
(field) =>
isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION) &&
(field as FieldMetadataEntity<FieldMetadataType.RELATION>).settings
?.relationType === RelationType.ONE_TO_MANY,
);
const relationFieldsToDelete = [
...manyToOneRelationFieldsToDelete,
...(oneToManyRelationFieldsToDelete.map(
(field) => field.relationTargetFieldMetadata,
) as FieldMetadataEntity<FieldMetadataType.RELATION>[]),
];
for (const relationFieldToDelete of relationFieldsToDelete) {
const joinColumnName = relationFieldToDelete.settings?.joinColumnName;
if (!joinColumnName) {
throw new Error(
`Join column name is not set for relation field ${relationFieldToDelete.name}`,
);
}
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`delete-${RELATION_MIGRATION_PRIORITY_PREFIX}-${relationFieldToDelete.name}`,
),
workspaceId,
[
{
name: computeTableName(
relationFieldToDelete.object.nameSingular,
relationFieldToDelete.object.isCustom,
),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: joinColumnName,
} satisfies WorkspaceMigrationColumnDrop,
],
},
],
);
}
}
// DROP TABLE // DROP TABLE
await this.workspaceMigrationService.createCustomMigration( await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`delete-${objectMetadata.nameSingular}`), generateMigrationName(`delete-${objectMetadata.nameSingular}`),

View File

@ -23,6 +23,7 @@ export const buildMigrationsForCustomObjectRelations = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
// TODO: When we get rid of this and use the sync metadata, columnName must be based on the joinColumnName from the field metadata settings
columnName: computeColumnName(createdObjectMetadata.nameSingular, { columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true, isForeignKey: true,
}), }),
@ -38,6 +39,7 @@ export const buildMigrationsForCustomObjectRelations = (
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
// TODO: When we get rid of this and use the sync metadata, columnName must be based on the joinColumnName from the field metadata settings
columnName: computeColumnName(createdObjectMetadata.nameSingular, { columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true, isForeignKey: true,
}), }),

View File

@ -3,13 +3,17 @@ import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import camelCase from 'lodash.camelcase'; import camelCase from 'lodash.camelcase';
import { FindOneOptions, In, Repository } from 'typeorm';
import { v4 as uuidV4 } from 'uuid';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { FindOneOptions, In, Repository } from 'typeorm';
import { v4 as uuidV4 } from 'uuid';
import { FieldMetadataDefaultSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
@ -20,6 +24,7 @@ import {
RelationMetadataException, RelationMetadataException,
RelationMetadataExceptionCode, RelationMetadataExceptionCode,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.exception'; } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.exception';
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception';
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
@ -34,7 +39,6 @@ import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { BASE_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { BASE_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception';
import { import {
RelationMetadataEntity, RelationMetadataEntity,
@ -56,6 +60,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly indexMetadataService: IndexMetadataService, private readonly indexMetadataService: IndexMetadataService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly featureFlagService: FeatureFlagService,
) { ) {
super(relationMetadataRepository); super(relationMetadataRepository);
} }
@ -67,6 +72,11 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
relationMetadataInput, relationMetadataInput,
); );
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
relationMetadataInput.workspaceId,
);
try { try {
validateMetadataNameOrThrow(relationMetadataInput.fromName); validateMetadataNameOrThrow(relationMetadataInput.fromName);
validateMetadataNameOrThrow(relationMetadataInput.toName); validateMetadataNameOrThrow(relationMetadataInput.toName);
@ -92,21 +102,31 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
const toId = uuidV4(); const toId = uuidV4();
const createdRelationFieldsMetadata = const createdRelationFieldsMetadata =
await this.fieldMetadataRepository.save([ await this.fieldMetadataRepository.save(
this.createFieldMetadataForRelationMetadata( [
relationMetadataInput, this.createFieldMetadataForRelationMetadata(
'from', relationMetadataInput,
isCustom, 'from',
fromId, isCustom,
), fromId,
this.createFieldMetadataForRelationMetadata( ),
relationMetadataInput, this.createFieldMetadataForRelationMetadata(
'to', relationMetadataInput,
isCustom, 'to',
toId, isCustom,
), toId,
this.createForeignKeyFieldMetadata(relationMetadataInput, columnName), ),
]); // We don't want to create the join column field metadata for new relation
...(isNewRelationEnabled
? []
: [
this.createForeignKeyFieldMetadata(
relationMetadataInput,
columnName,
),
]),
].flat(),
);
const createdRelationMetadata = await super.createOne({ const createdRelationMetadata = await super.createOne({
...relationMetadataInput, ...relationMetadataInput,
@ -114,6 +134,46 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
toFieldMetadataId: toId, toFieldMetadataId: toId,
}); });
const fromFieldMetadata = createdRelationFieldsMetadata.find(
(fieldMetadata) => fieldMetadata.id === fromId,
);
if (!fromFieldMetadata) {
throw new RelationMetadataException(
`From field metadata not found`,
RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND,
);
}
const toFieldMetadata = createdRelationFieldsMetadata.find(
(fieldMetadata) => fieldMetadata.id === toId,
);
if (!toFieldMetadata) {
throw new RelationMetadataException(
`To field metadata not found`,
RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND,
);
}
await this.fieldMetadataRepository.update(fromId, {
settings: {
relationType: RelationType.ONE_TO_MANY,
} as Partial<FieldMetadataDefaultSettings>,
relationTargetFieldMetadataId: toId,
relationTargetObjectMetadataId: relationMetadataInput.toObjectMetadataId,
});
await this.fieldMetadataRepository.update(toId, {
settings: {
relationType: RelationType.MANY_TO_ONE,
joinColumnName: `${relationMetadataInput.toName}Id`,
} as Partial<FieldMetadataDefaultSettings>,
relationTargetFieldMetadataId: fromId,
relationTargetObjectMetadataId:
relationMetadataInput.fromObjectMetadataId,
});
await this.createWorkspaceCustomMigration( await this.createWorkspaceCustomMigration(
relationMetadataInput, relationMetadataInput,
objectMetadataMap, objectMetadataMap,
@ -123,17 +183,6 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
const toObjectMetadata = const toObjectMetadata =
objectMetadataMap[relationMetadataInput.toObjectMetadataId]; objectMetadataMap[relationMetadataInput.toObjectMetadataId];
const foreignKeyFieldMetadata = createdRelationFieldsMetadata.find(
(fieldMetadata) => fieldMetadata.type === FieldMetadataType.UUID,
);
if (!foreignKeyFieldMetadata) {
throw new RelationMetadataException(
`ForeignKey field metadata not found`,
RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND,
);
}
const deletedAtFieldMetadata = toObjectMetadata.fields.find( const deletedAtFieldMetadata = toObjectMetadata.fields.find(
(fieldMetadata) => (fieldMetadata) =>
fieldMetadata.standardId === BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt, fieldMetadata.standardId === BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
@ -141,16 +190,29 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
this.throwIfDeletedAtFieldMetadataNotFound(deletedAtFieldMetadata); this.throwIfDeletedAtFieldMetadataNotFound(deletedAtFieldMetadata);
await this.indexMetadataService.createIndexMetadata( if (!isNewRelationEnabled) {
relationMetadataInput.workspaceId, const foreignKeyFieldMetadata = createdRelationFieldsMetadata.find(
toObjectMetadata, (fieldMetadata) => fieldMetadata.type === FieldMetadataType.UUID,
[ );
foreignKeyFieldMetadata,
deletedAtFieldMetadata as FieldMetadataEntity<FieldMetadataType>, if (!foreignKeyFieldMetadata) {
], throw new RelationMetadataException(
false, `ForeignKey field metadata not found`,
false, RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND,
); );
}
await this.indexMetadataService.createIndexMetadata(
relationMetadataInput.workspaceId,
toObjectMetadata,
[
foreignKeyFieldMetadata,
deletedAtFieldMetadata as FieldMetadataEntity<FieldMetadataType>,
],
false,
false,
);
}
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
relationMetadataInput.workspaceId, relationMetadataInput.workspaceId,

View File

@ -5,4 +5,5 @@ import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metada
export type ObjectMetadataItemWithFieldMaps = ObjectMetadataInterface & { export type ObjectMetadataItemWithFieldMaps = ObjectMetadataInterface & {
fieldsById: FieldMetadataMap; fieldsById: FieldMetadataMap;
fieldsByName: FieldMetadataMap; fieldsByName: FieldMetadataMap;
fieldsByJoinColumnName: FieldMetadataMap;
}; };

View File

@ -1,8 +1,11 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
export const generateObjectMetadataMaps = ( export const generateObjectMetadataMaps = (
objectMetadataCollection: ObjectMetadataInterface[], objectMetadataCollection: ObjectMetadataInterface[],
@ -15,8 +18,21 @@ export const generateObjectMetadataMaps = (
for (const objectMetadata of objectMetadataCollection) { for (const objectMetadata of objectMetadataCollection) {
const fieldsByIdMap: FieldMetadataMap = {}; const fieldsByIdMap: FieldMetadataMap = {};
const fieldsByNameMap: FieldMetadataMap = {}; const fieldsByNameMap: FieldMetadataMap = {};
const fieldsByJoinColumnNameMap: FieldMetadataMap = {};
for (const fieldMetadata of objectMetadata.fields) { for (const fieldMetadata of objectMetadata.fields) {
if (
isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
)
) {
if (fieldMetadata.settings?.joinColumnName) {
fieldsByJoinColumnNameMap[fieldMetadata.settings.joinColumnName] =
fieldMetadata;
}
}
fieldsByNameMap[fieldMetadata.name] = fieldMetadata; fieldsByNameMap[fieldMetadata.name] = fieldMetadata;
fieldsByIdMap[fieldMetadata.id] = fieldMetadata; fieldsByIdMap[fieldMetadata.id] = fieldMetadata;
} }
@ -25,6 +41,7 @@ export const generateObjectMetadataMaps = (
...objectMetadata, ...objectMetadata,
fieldsById: fieldsByIdMap, fieldsById: fieldsByIdMap,
fieldsByName: fieldsByNameMap, fieldsByName: fieldsByNameMap,
fieldsByJoinColumnName: fieldsByJoinColumnNameMap,
}; };
objectMetadataMaps.byId[objectMetadata.id] = processedObjectMetadata; objectMetadataMaps.byId[objectMetadata.id] = processedObjectMetadata;

View File

@ -1,6 +1,7 @@
import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory'; import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory';
import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
import { RelationColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory';
import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory';
export const workspaceColumnActionFactories = [ export const workspaceColumnActionFactories = [
@ -8,4 +9,5 @@ export const workspaceColumnActionFactories = [
BasicColumnActionFactory, BasicColumnActionFactory,
EnumColumnActionFactory, EnumColumnActionFactory,
CompositeColumnActionFactory, CompositeColumnActionFactory,
RelationColumnActionFactory,
]; ];

View File

@ -0,0 +1,97 @@
import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface';
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnAlter,
WorkspaceMigrationColumnCreate,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import {
WorkspaceMigrationException,
WorkspaceMigrationExceptionCode,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception';
@Injectable()
export class RelationColumnActionFactory extends ColumnActionAbstractFactory<FieldMetadataType.RELATION> {
protected readonly logger = new Logger(RelationColumnActionFactory.name);
protected handleCreateAction(
fieldMetadata: FieldMetadataInterface<FieldMetadataType.RELATION>,
_options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnCreate[] {
if (!fieldMetadata.settings || !fieldMetadata.settings.joinColumnName) {
return [];
}
const joinColumnName = fieldMetadata.settings.joinColumnName;
return [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: joinColumnName,
columnType: fieldMetadataTypeToColumnType(FieldMetadataType.UUID),
isArray: false,
isNullable: fieldMetadata.isNullable ?? true,
isUnique:
fieldMetadata.settings.relationType === RelationType.ONE_TO_ONE,
defaultValue: null,
},
];
}
protected handleAlterAction(
currentFieldMetadata: FieldMetadataInterface<FieldMetadataType.RELATION>,
alteredFieldMetadata: FieldMetadataInterface<FieldMetadataType.RELATION>,
_options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAlter[] {
if (!currentFieldMetadata.settings || !alteredFieldMetadata.settings) {
return [];
}
const currentJoinColumnName = currentFieldMetadata.settings.joinColumnName;
const alteredJoinColumnName = alteredFieldMetadata.settings.joinColumnName;
if (!currentJoinColumnName || !alteredJoinColumnName) {
this.logger.error(
`Column name not found for current or altered field metadata, can be due to a missing or an invalid target column map. Current column name: ${currentJoinColumnName}, Altered column name: ${alteredJoinColumnName}.`,
);
throw new WorkspaceMigrationException(
`Column name not found for current or altered field metadata`,
WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA,
);
}
return [
{
action: WorkspaceMigrationColumnActionType.ALTER,
currentColumnDefinition: {
columnName: currentJoinColumnName,
columnType: fieldMetadataTypeToColumnType(FieldMetadataType.UUID),
isArray: false,
isNullable: currentFieldMetadata.isNullable ?? true,
isUnique:
currentFieldMetadata.settings.relationType ===
RelationType.ONE_TO_ONE,
defaultValue: null,
},
alteredColumnDefinition: {
columnName: alteredJoinColumnName,
columnType: fieldMetadataTypeToColumnType(FieldMetadataType.UUID),
isArray: false,
isNullable: alteredFieldMetadata.isNullable ?? true,
isUnique:
alteredFieldMetadata.settings.relationType ===
RelationType.ONE_TO_ONE,
defaultValue: null,
},
},
];
}
}

View File

@ -9,6 +9,7 @@ import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/worksp
import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory'; import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory';
import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
import { RelationColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory';
import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory';
import { import {
WorkspaceMigrationColumnAction, WorkspaceMigrationColumnAction,
@ -35,6 +36,7 @@ export class WorkspaceMigrationFactory {
private readonly tsVectorColumnActionFactory: TsVectorColumnActionFactory, private readonly tsVectorColumnActionFactory: TsVectorColumnActionFactory,
private readonly enumColumnActionFactory: EnumColumnActionFactory, private readonly enumColumnActionFactory: EnumColumnActionFactory,
private readonly compositeColumnActionFactory: CompositeColumnActionFactory, private readonly compositeColumnActionFactory: CompositeColumnActionFactory,
private readonly relationColumnActionFactory: RelationColumnActionFactory,
) { ) {
this.factoriesMap = new Map< this.factoriesMap = new Map<
FieldMetadataType, FieldMetadataType,
@ -98,6 +100,10 @@ export class WorkspaceMigrationFactory {
FieldMetadataType.RICH_TEXT_V2, FieldMetadataType.RICH_TEXT_V2,
{ factory: this.compositeColumnActionFactory }, { factory: this.compositeColumnActionFactory },
], ],
[
FieldMetadataType.RELATION,
{ factory: this.relationColumnActionFactory },
],
]); ]);
} }

View File

@ -1,14 +1,13 @@
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator'; import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator';
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
@ -67,7 +66,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceRelation({ @WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.noteTargets, standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.noteTargets,
label: msg`Notes`, label: msg`Notes`,
type: RelationMetadataType.ONE_TO_MANY, type: RelationType.ONE_TO_MANY,
description: (objectMetadata) => { description: (objectMetadata) => {
const label = objectMetadata.labelSingular; const label = objectMetadata.labelSingular;
@ -83,7 +82,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceRelation({ @WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.taskTargets, standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.taskTargets,
label: msg`Tasks`, label: msg`Tasks`,
type: RelationMetadataType.ONE_TO_MANY, type: RelationType.ONE_TO_MANY,
description: (objectMetadata) => { description: (objectMetadata) => {
const label = objectMetadata.labelSingular; const label = objectMetadata.labelSingular;
@ -99,7 +98,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceRelation({ @WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.favorites, standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.favorites,
label: msg`Favorites`, label: msg`Favorites`,
type: RelationMetadataType.ONE_TO_MANY, type: RelationType.ONE_TO_MANY,
description: (objectMetadata) => { description: (objectMetadata) => {
const label = objectMetadata.labelSingular; const label = objectMetadata.labelSingular;
@ -116,7 +115,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceRelation({ @WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.attachments, standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.attachments,
label: msg`Attachments`, label: msg`Attachments`,
type: RelationMetadataType.ONE_TO_MANY, type: RelationType.ONE_TO_MANY,
description: (objectMetadata) => { description: (objectMetadata) => {
const label = objectMetadata.labelSingular; const label = objectMetadata.labelSingular;
@ -132,7 +131,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceRelation({ @WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.timelineActivities, standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.timelineActivities,
label: msg`Timeline Activities`, label: msg`Timeline Activities`,
type: RelationMetadataType.ONE_TO_MANY, type: RelationType.ONE_TO_MANY,
description: (objectMetadata) => { description: (objectMetadata) => {
const label = objectMetadata.labelSingular; const label = objectMetadata.labelSingular;

View File

@ -1,16 +1,14 @@
import { ObjectType } from 'typeorm'; import { ObjectType } from 'typeorm';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { WorkspaceDynamicRelationMetadataArgsFactory } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface'; import { WorkspaceDynamicRelationMetadataArgsFactory } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { TypedReflect } from 'src/utils/typed-reflect'; import { TypedReflect } from 'src/utils/typed-reflect';
interface WorkspaceBaseDynamicRelationOptions<TClass> { interface WorkspaceBaseDynamicRelationOptions<TClass> {
type: RelationMetadataType; type: RelationType;
argsFactory: WorkspaceDynamicRelationMetadataArgsFactory; argsFactory: WorkspaceDynamicRelationMetadataArgsFactory;
inverseSideTarget: () => ObjectType<TClass>; inverseSideTarget: () => ObjectType<TClass>;
inverseSideFieldKey?: keyof TClass; inverseSideFieldKey?: keyof TClass;

View File

@ -28,6 +28,15 @@ export function WorkspaceFieldIndex(
...additionalDefaultColumnsForIndex, ...additionalDefaultColumnsForIndex,
]; ];
// TODO: Remove this when we are handling properly indexes for new relation metadata
if (
process.env.SYNC_METADATA_INDEX_ENABLED === 'false' ||
process.env.SYNC_METADATA_INDEX_ENABLED === '' ||
process.env.SYNC_METADATA_INDEX_ENABLED === undefined
) {
return;
}
metadataArgsStorage.addIndexes({ metadataArgsStorage.addIndexes({
name: `IDX_${generateDeterministicIndexName([ name: `IDX_${generateDeterministicIndexName([
convertClassNameToObjectMetadataName(target.constructor.name), convertClassNameToObjectMetadataName(target.constructor.name),

View File

@ -11,7 +11,11 @@ import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args
import { TypedReflect } from 'src/utils/typed-reflect'; import { TypedReflect } from 'src/utils/typed-reflect';
export interface WorkspaceFieldOptions< export interface WorkspaceFieldOptions<
T extends FieldMetadataType = FieldMetadataType, T extends FieldMetadataType = Exclude<
FieldMetadataType,
// Use @WorkspaceRelation or @WorkspaceDynamicRelation for relation fields
FieldMetadataType.RELATION
>,
> { > {
standardId: string; standardId: string;
type: T; type: T;

View File

@ -18,6 +18,15 @@ export function WorkspaceIndex(
throw new Error('Class level WorkspaceIndex should be used with columns'); throw new Error('Class level WorkspaceIndex should be used with columns');
} }
// TODO: Remove this when we are handling properly indexes for new relation metadata
if (
process.env.SYNC_METADATA_INDEX_ENABLED === 'false' ||
process.env.SYNC_METADATA_INDEX_ENABLED === '' ||
process.env.SYNC_METADATA_INDEX_ENABLED === undefined
) {
return (_target: any) => {};
}
return (target: any) => { return (target: any) => {
const gate = TypedReflect.getMetadata( const gate = TypedReflect.getMetadata(
'workspace:gate-metadata-args', 'workspace:gate-metadata-args',

View File

@ -17,17 +17,20 @@ export function WorkspaceIsUnique(): PropertyDecorator {
const columns = [propertyKey.toString()]; const columns = [propertyKey.toString()];
metadataArgsStorage.addIndexes({ // TODO: Remove this when we are handling properly indexes for new relation metadata
name: `IDX_UNIQUE_${generateDeterministicIndexName([ if (process.env.SYNC_METADATA_INDEX_ENABLED === 'true') {
convertClassNameToObjectMetadataName(target.constructor.name), metadataArgsStorage.addIndexes({
...columns, name: `IDX_UNIQUE_${generateDeterministicIndexName([
])}`, convertClassNameToObjectMetadataName(target.constructor.name),
columns, ...columns,
target: target.constructor, ])}`,
gate, columns,
isUnique: true, target: target.constructor,
whereClause: null, gate,
}); isUnique: true,
whereClause: null,
});
}
return TypedReflect.defineMetadata( return TypedReflect.defineMetadata(
'workspace:is-unique-metadata-args', 'workspace:is-unique-metadata-args',

View File

@ -1,15 +1,14 @@
import { MessageDescriptor } from '@lingui/core'; import { MessageDescriptor } from '@lingui/core';
import { ObjectType } from 'typeorm'; import { ObjectType } from 'typeorm';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { TypedReflect } from 'src/utils/typed-reflect'; import { TypedReflect } from 'src/utils/typed-reflect';
interface WorkspaceRelationOptions<TClass> { interface WorkspaceRelationBaseOptions<TClass> {
standardId: string; standardId: string;
label: label:
| MessageDescriptor | MessageDescriptor
@ -18,12 +17,26 @@ interface WorkspaceRelationOptions<TClass> {
| MessageDescriptor | MessageDescriptor
| ((objectMetadata: ObjectMetadataEntity) => MessageDescriptor); | ((objectMetadata: ObjectMetadataEntity) => MessageDescriptor);
icon?: string; icon?: string;
type: RelationMetadataType;
inverseSideTarget: () => ObjectType<TClass>; inverseSideTarget: () => ObjectType<TClass>;
inverseSideFieldKey?: keyof TClass; inverseSideFieldKey?: keyof TClass;
onDelete?: RelationOnDeleteAction; onDelete?: RelationOnDeleteAction;
} }
interface WorkspaceOtherRelationOptions<TClass>
extends WorkspaceRelationBaseOptions<TClass> {
type: RelationType.ONE_TO_MANY | RelationType.ONE_TO_ONE;
}
interface WorkspaceManyToOneRelationOptions<TClass extends object>
extends WorkspaceRelationBaseOptions<TClass> {
type: RelationType.MANY_TO_ONE;
inverseSideFieldKey: keyof TClass;
}
type WorkspaceRelationOptions<TClass extends object> =
| WorkspaceOtherRelationOptions<TClass>
| WorkspaceManyToOneRelationOptions<TClass>;
export function WorkspaceRelation<TClass extends object>( export function WorkspaceRelation<TClass extends object>(
options: WorkspaceRelationOptions<TClass>, options: WorkspaceRelationOptions<TClass>,
): PropertyDecorator { ): PropertyDecorator {

View File

@ -14,4 +14,5 @@ export enum TwentyORMExceptionCode {
ROLES_PERMISSIONS_VERSION_NOT_FOUND = 'ROLES_PERMISSIONS_VERSION_NOT_FOUND', ROLES_PERMISSIONS_VERSION_NOT_FOUND = 'ROLES_PERMISSIONS_VERSION_NOT_FOUND',
FEATURE_FLAG_MAP_VERSION_NOT_FOUND = 'FEATURE_FLAG_MAP_VERSION_NOT_FOUND', FEATURE_FLAG_MAP_VERSION_NOT_FOUND = 'FEATURE_FLAG_MAP_VERSION_NOT_FOUND',
USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND = 'USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND', USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND = 'USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND',
MALFORMED_METADATA = 'MALFORMED_METADATA',
} }

View File

@ -1,9 +1,11 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ColumnType, EntitySchemaColumnOptions } from 'typeorm';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ColumnType, EntitySchemaColumnOptions } from 'typeorm';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
@ -12,7 +14,11 @@ import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metad
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
type EntitySchemaColumnMap = { type EntitySchemaColumnMap = {
[key: string]: EntitySchemaColumnOptions; [key: string]: EntitySchemaColumnOptions;
@ -20,7 +26,10 @@ type EntitySchemaColumnMap = {
@Injectable() @Injectable()
export class EntitySchemaColumnFactory { export class EntitySchemaColumnFactory {
create(fieldMetadataMapByName: FieldMetadataMap): EntitySchemaColumnMap { create(
fieldMetadataMapByName: FieldMetadataMap,
isNewRelationEnabled: boolean,
): EntitySchemaColumnMap {
let entitySchemaColumnMap: EntitySchemaColumnMap = {}; let entitySchemaColumnMap: EntitySchemaColumnMap = {};
const fieldMetadataCollection = Object.values(fieldMetadataMapByName); const fieldMetadataCollection = Object.values(fieldMetadataMapByName);
@ -28,33 +37,63 @@ export class EntitySchemaColumnFactory {
for (const fieldMetadata of fieldMetadataCollection) { for (const fieldMetadata of fieldMetadataCollection) {
const key = fieldMetadata.name; const key = fieldMetadata.name;
if (isRelationFieldMetadataType(fieldMetadata.type)) { if (
const relationMetadata = isFieldMetadataInterfaceOfType(
fieldMetadata.fromRelationMetadata ?? fieldMetadata,
fieldMetadata.toRelationMetadata; FieldMetadataType.RELATION,
if (!relationMetadata) {
throw new Error(
`Relation metadata is missing for field ${fieldMetadata.name}`,
);
}
const joinColumnKey = fieldMetadata.name + 'Id';
const joinColumn = fieldMetadataCollection.find(
(field) => field.name === joinColumnKey,
) )
? joinColumnKey ) {
: null; if (!isNewRelationEnabled) {
const relationMetadata =
fieldMetadata.fromRelationMetadata ??
fieldMetadata.toRelationMetadata;
if (joinColumn) { if (!relationMetadata) {
entitySchemaColumnMap[joinColumn] = { throw new Error(
name: joinColumn, `Relation metadata is missing for field ${fieldMetadata.name}`,
);
}
const joinColumnKey = fieldMetadata.name + 'Id';
const joinColumn = fieldMetadataCollection.find(
(field) => field.name === joinColumnKey,
)
? joinColumnKey
: null;
if (joinColumn) {
entitySchemaColumnMap[joinColumn] = {
name: joinColumn,
type: 'uuid',
nullable: fieldMetadata.isNullable,
};
}
continue;
} else {
const isManyToOneRelation =
fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE;
const joinColumnName = fieldMetadata.settings?.joinColumnName;
if (!isManyToOneRelation) {
continue;
}
if (!isDefined(joinColumnName)) {
throw new TwentyORMException(
`Field ${fieldMetadata.id} of type ${fieldMetadata.type} is a many to one relation but does not have a join column name`,
TwentyORMExceptionCode.MALFORMED_METADATA,
);
}
entitySchemaColumnMap[joinColumnName] = {
name: joinColumnName,
type: 'uuid', type: 'uuid',
nullable: fieldMetadata.isNullable, nullable: fieldMetadata.isNullable,
}; };
}
continue; continue;
}
} }
if (isCompositeFieldMetadataType(fieldMetadata.type)) { if (isCompositeFieldMetadataType(fieldMetadata.type)) {

View File

@ -1,11 +1,13 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { EntitySchemaRelationOptions } from 'typeorm'; import { EntitySchemaRelationOptions } from 'typeorm';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { determineRelationDetails } from 'src/engine/twenty-orm/utils/determine-relation-details.util'; import { determineRelationDetails } from 'src/engine/twenty-orm/utils/determine-relation-details.util';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; import { determineSchemaRelationDetails } from 'src/engine/twenty-orm/utils/determine-schema-relation-details.util';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
type EntitySchemaRelationMap = { type EntitySchemaRelationMap = {
[key: string]: EntitySchemaRelationOptions; [key: string]: EntitySchemaRelationOptions;
@ -18,37 +20,64 @@ export class EntitySchemaRelationFactory {
async create( async create(
fieldMetadataMapByName: FieldMetadataMap, fieldMetadataMapByName: FieldMetadataMap,
objectMetadataMaps: ObjectMetadataMaps, objectMetadataMaps: ObjectMetadataMaps,
isNewRelationEnabled: boolean,
): Promise<EntitySchemaRelationMap> { ): Promise<EntitySchemaRelationMap> {
const entitySchemaRelationMap: EntitySchemaRelationMap = {}; const entitySchemaRelationMap: EntitySchemaRelationMap = {};
const fieldMetadataCollection = Object.values(fieldMetadataMapByName); const fieldMetadataCollection = Object.values(fieldMetadataMapByName);
for (const fieldMetadata of fieldMetadataCollection) { for (const fieldMetadata of fieldMetadataCollection) {
if (!isRelationFieldMetadataType(fieldMetadata.type)) { if (
!isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
)
) {
continue; continue;
} }
const relationMetadata = if (!isNewRelationEnabled) {
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata; const relationMetadata =
fieldMetadata.fromRelationMetadata ??
fieldMetadata.toRelationMetadata;
if (!relationMetadata) { if (!relationMetadata) {
throw new Error( throw new Error(
`Relation metadata is missing for field ${fieldMetadata.name}`, `Relation metadata is missing for field ${fieldMetadata.name}`,
);
}
const relationDetails = await determineRelationDetails(
fieldMetadata,
relationMetadata,
objectMetadataMaps,
); );
entitySchemaRelationMap[fieldMetadata.name] = {
type: relationDetails.relationType,
target: relationDetails.target,
inverseSide: relationDetails.inverseSide,
joinColumn: relationDetails.joinColumn,
} satisfies EntitySchemaRelationOptions;
} else {
if (!fieldMetadata.settings) {
throw new Error(
`Field metadata settings are missing for field ${fieldMetadata.name}`,
);
}
const schemaRelationDetails = await determineSchemaRelationDetails(
fieldMetadata,
objectMetadataMaps,
);
entitySchemaRelationMap[fieldMetadata.name] = {
type: schemaRelationDetails.relationType,
target: schemaRelationDetails.target,
inverseSide: schemaRelationDetails.inverseSide,
joinColumn: schemaRelationDetails.joinColumn,
} satisfies EntitySchemaRelationOptions;
} }
const relationDetails = await determineRelationDetails(
fieldMetadata,
relationMetadata,
objectMetadataMaps,
);
entitySchemaRelationMap[fieldMetadata.name] = {
type: relationDetails.relationType,
target: relationDetails.target,
inverseSide: relationDetails.inverseSide,
joinColumn: relationDetails.joinColumn,
} satisfies EntitySchemaRelationOptions;
} }
return entitySchemaRelationMap; return entitySchemaRelationMap;

View File

@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common';
import { EntitySchema } from 'typeorm'; import { EntitySchema } from 'typeorm';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory'; import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory';
@ -14,6 +16,7 @@ export class EntitySchemaFactory {
constructor( constructor(
private readonly entitySchemaColumnFactory: EntitySchemaColumnFactory, private readonly entitySchemaColumnFactory: EntitySchemaColumnFactory,
private readonly entitySchemaRelationFactory: EntitySchemaRelationFactory, private readonly entitySchemaRelationFactory: EntitySchemaRelationFactory,
private readonly featureFlagService: FeatureFlagService,
) {} ) {}
async create( async create(
@ -22,13 +25,20 @@ export class EntitySchemaFactory {
objectMetadata: ObjectMetadataItemWithFieldMaps, objectMetadata: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps, objectMetadataMaps: ObjectMetadataMaps,
): Promise<EntitySchema> { ): Promise<EntitySchema> {
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
const columns = this.entitySchemaColumnFactory.create( const columns = this.entitySchemaColumnFactory.create(
objectMetadata.fieldsByName, objectMetadata.fieldsByName,
isNewRelationEnabled,
); );
const relations = await this.entitySchemaRelationFactory.create( const relations = await this.entitySchemaRelationFactory.create(
objectMetadata.fieldsByName, objectMetadata.fieldsByName,
objectMetadataMaps, objectMetadataMaps,
isNewRelationEnabled,
); );
const entitySchema = new EntitySchema({ const entitySchema = new EntitySchema({

View File

@ -147,6 +147,7 @@ export class WorkspaceDatasourceFactory {
{ {
workspaceId, workspaceId,
objectMetadataMaps: cachedObjectMetadataMaps, objectMetadataMaps: cachedObjectMetadataMaps,
featureFlagsMap: cachedFeatureFlagMap,
}, },
{ {
url: url:

View File

@ -1,12 +1,10 @@
import { ObjectType } from 'typeorm'; import { ObjectType } from 'typeorm';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface'; import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
export type WorkspaceDynamicRelationMetadataArgsFactory = ( export type WorkspaceDynamicRelationMetadataArgsFactory = (
oppositeObjectMetadata: ObjectMetadataEntity, oppositeObjectMetadata: ObjectMetadataEntity,
@ -57,7 +55,7 @@ export interface WorkspaceDynamicRelationMetadataArgs {
/** /**
* Relation type. * Relation type.
*/ */
readonly type: RelationMetadataType; readonly type: RelationType;
/** /**
* Relation inverse side target. * Relation inverse side target.

View File

@ -1,6 +1,8 @@
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export interface WorkspaceInternalContext { export interface WorkspaceInternalContext {
workspaceId: string; workspaceId: string;
objectMetadataMaps: ObjectMetadataMaps; objectMetadataMaps: ObjectMetadataMaps;
featureFlagsMap: Record<FeatureFlagKey, boolean>;
} }

View File

@ -1,12 +1,10 @@
import { ObjectType } from 'typeorm'; import { ObjectType } from 'typeorm';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface'; import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
export interface WorkspaceRelationMetadataArgs { export interface WorkspaceRelationMetadataArgs {
/** /**
@ -33,7 +31,7 @@ export interface WorkspaceRelationMetadataArgs {
/** /**
* Relation type. * Relation type.
*/ */
readonly type: RelationMetadataType; readonly type: RelationType;
/** /**
* Relation description. * Relation description.

View File

@ -715,7 +715,14 @@ export class WorkspaceRepository<
objectMetadata ??= await this.getObjectMetadataFromTarget(); objectMetadata ??= await this.getObjectMetadataFromTarget();
const objectMetadataMaps = this.internalContext.objectMetadataMaps; const objectMetadataMaps = this.internalContext.objectMetadataMaps;
const isNewRelationEnabled =
this.internalContext.featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled];
return formatResult(data, objectMetadata, objectMetadataMaps) as T; return formatResult(
data,
objectMetadata,
objectMetadataMaps,
isNewRelationEnabled,
) as T;
} }
} }

View File

@ -0,0 +1,14 @@
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
export const converRelationTypeToTypeORMRelationType = (type: RelationType) => {
switch (type) {
case RelationType.ONE_TO_MANY:
return 'one-to-many';
case RelationType.MANY_TO_ONE:
return 'many-to-one';
case RelationType.ONE_TO_ONE:
return 'one-to-one';
default:
throw new Error(`Invalid relation type: ${type}`);
}
};

View File

@ -0,0 +1,62 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'typeorm/metadata/types/RelationTypes';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { converRelationTypeToTypeORMRelationType } from 'src/engine/twenty-orm/utils/convert-relation-type-to-typeorm-relation-type.util';
interface RelationDetails {
relationType: RelationType;
target: string;
inverseSide: string;
joinColumn: { name: string } | undefined;
}
export async function determineSchemaRelationDetails(
fieldMetadata: FieldMetadataInterface<FieldMetadataType.RELATION>,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<RelationDetails> {
if (!fieldMetadata.settings) {
throw new Error('Field metadata settings are missing');
}
const relationType = converRelationTypeToTypeORMRelationType(
fieldMetadata.settings.relationType,
);
if (!fieldMetadata.relationTargetObjectMetadataId) {
throw new Error('Relation target object metadata ID is missing');
}
const sourceObjectMetadata =
objectMetadataMaps.byId[fieldMetadata.objectMetadataId];
const targetObjectMetadata =
objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId];
if (!sourceObjectMetadata || !targetObjectMetadata) {
throw new Error('Object metadata not found');
}
if (!fieldMetadata.relationTargetFieldMetadataId) {
throw new Error('Relation target field metadata ID is missing');
}
const targetFieldMetadata =
targetObjectMetadata.fieldsById[
fieldMetadata.relationTargetFieldMetadataId
];
if (!targetFieldMetadata) {
throw new Error('Target field metadata not found');
}
return {
relationType,
target: targetObjectMetadata.nameSingular,
inverseSide: targetFieldMetadata.name,
joinColumn: fieldMetadata.settings.joinColumnName
? { name: fieldMetadata.settings.joinColumnName }
: undefined,
};
}

View File

@ -7,6 +7,7 @@ import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-meta
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
export function formatData<T>( export function formatData<T>(
data: T, data: T,
@ -23,9 +24,28 @@ export function formatData<T>(
} }
const newData: Record<string, any> = {}; const newData: Record<string, any> = {};
const fieldMetadataByJoinColumnName =
objectMetadataItemWithFieldMaps.fields.reduce((acc, fieldMetadata) => {
if (
isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
)
) {
const joinColumnName = fieldMetadata.settings?.joinColumnName;
if (joinColumnName) {
acc.set(joinColumnName, fieldMetadata);
}
}
return acc;
}, new Map<string, FieldMetadataInterface>());
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsByName[key]; const fieldMetadata =
objectMetadataItemWithFieldMaps.fieldsByName[key] ||
fieldMetadataByJoinColumnName.get(key);
if (!fieldMetadata) { if (!fieldMetadata) {
throw new Error( throw new Error(

View File

@ -13,7 +13,7 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util'; import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util';
import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection'; import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { isDate } from 'src/utils/date/isDate'; import { isDate } from 'src/utils/date/isDate';
import { isValidDate } from 'src/utils/date/isValidDate'; import { isValidDate } from 'src/utils/date/isValidDate';
@ -21,6 +21,7 @@ export function formatResult<T>(
data: any, data: any,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps, objectMetadataMaps: ObjectMetadataMaps,
isNewRelationEnabled: boolean,
): T { ): T {
if (!data) { if (!data) {
return data; return data;
@ -28,7 +29,12 @@ export function formatResult<T>(
if (Array.isArray(data)) { if (Array.isArray(data)) {
return data.map((item) => return data.map((item) =>
formatResult(item, objectMetadataItemWithFieldMaps, objectMetadataMaps), formatResult(
item,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
isNewRelationEnabled,
),
) as T; ) as T;
} }
@ -44,38 +50,57 @@ export function formatResult<T>(
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
); );
const relationMetadataMap = new Map( const relationMetadataMap: Map<
Object.values(objectMetadataItemWithFieldMaps.fieldsById) string,
.filter(({ type }) => isRelationFieldMetadataType(type)) {
.map((fieldMetadata) => [ relationMetadata: RelationMetadataEntity | undefined;
fieldMetadata.name, relationType: string;
{ }
relationMetadata: > = isNewRelationEnabled
fieldMetadata.fromRelationMetadata ?? ? new Map()
fieldMetadata.toRelationMetadata, : new Map(
relationType: computeRelationType( Object.values(objectMetadataItemWithFieldMaps.fieldsById)
fieldMetadata, .filter((fieldMetadata) =>
fieldMetadata.fromRelationMetadata ?? isFieldMetadataInterfaceOfType(
(fieldMetadata.toRelationMetadata as RelationMetadataEntity), fieldMetadata,
), FieldMetadataType.RELATION,
}, ),
]), )
); .map((fieldMetadata) => [
fieldMetadata.name,
{
relationMetadata:
fieldMetadata.fromRelationMetadata ??
fieldMetadata.toRelationMetadata,
relationType: computeRelationType(
fieldMetadata,
fieldMetadata.fromRelationMetadata ??
(fieldMetadata.toRelationMetadata as RelationMetadataEntity),
),
},
]),
);
const newData: object = {}; const newData: object = {};
const objectMetadaItemFieldsByName = const objectMetadaItemFieldsByName =
objectMetadataMaps.byId[objectMetadataItemWithFieldMaps.id]?.fieldsByName; objectMetadataMaps.byId[objectMetadataItemWithFieldMaps.id]?.fieldsByName;
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
const compositePropertyArgs = compositeFieldMetadataMap.get(key); const compositePropertyArgs = compositeFieldMetadataMap.get(key);
const { relationMetadata, relationType } = const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsById[key];
relationMetadataMap.get(key) ?? {}; const isRelation = fieldMetadata
? isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
)
: false;
if (!compositePropertyArgs && !relationMetadata) { if (!compositePropertyArgs && !isRelation) {
if (isPlainObject(value)) { if (isPlainObject(value)) {
newData[key] = formatResult( newData[key] = formatResult(
value, value,
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
objectMetadataMaps, objectMetadataMaps,
isNewRelationEnabled,
); );
} else if (objectMetadaItemFieldsByName[key]) { } else if (objectMetadaItemFieldsByName[key]) {
newData[key] = formatFieldMetadataValue( newData[key] = formatFieldMetadataValue(
@ -89,31 +114,63 @@ export function formatResult<T>(
continue; continue;
} }
if (relationMetadata) { if (!isNewRelationEnabled) {
const toObjectMetadata = const { relationMetadata, relationType } =
objectMetadataMaps.byId[relationMetadata.toObjectMetadataId]; relationMetadataMap.get(key) ?? {};
const fromObjectMetadata = if (relationMetadata) {
objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId]; const toObjectMetadata =
objectMetadataMaps.byId[relationMetadata.toObjectMetadataId];
if (!toObjectMetadata) { const fromObjectMetadata =
throw new Error( objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId];
`Object metadata for object metadataId "${relationMetadata.toObjectMetadataId}" is missing`,
if (!toObjectMetadata) {
throw new Error(
`Object metadata for object metadataId "${relationMetadata.toObjectMetadataId}" is missing`,
);
}
if (!fromObjectMetadata) {
throw new Error(
`Object metadata for object metadataId "${relationMetadata.fromObjectMetadataId}" is missing`,
);
}
newData[key] = formatResult(
value,
relationType === 'one-to-many'
? toObjectMetadata
: fromObjectMetadata,
objectMetadataMaps,
isNewRelationEnabled,
);
continue;
}
} else {
if (isRelation) {
if (!fieldMetadata.relationTargetObjectMetadataId) {
throw new Error(
`Relation target object metadata ID is missing for field "${key}"`,
);
}
const targetObjectMetadata =
objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId];
if (!targetObjectMetadata) {
throw new Error(
`Object metadata for object metadataId "${fieldMetadata.relationTargetObjectMetadataId}" is missing`,
);
}
newData[key] = formatResult(
value,
targetObjectMetadata,
objectMetadataMaps,
isNewRelationEnabled,
); );
} }
if (!fromObjectMetadata) {
throw new Error(
`Object metadata for object metadataId "${relationMetadata.fromObjectMetadataId}" is missing`,
);
}
newData[key] = formatResult(
value,
relationType === 'one-to-many' ? toObjectMetadata : fromObjectMetadata,
objectMetadataMaps,
);
continue;
} }
if (!compositePropertyArgs) { if (!compositePropertyArgs) {

View File

@ -1,7 +1,7 @@
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface'; import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface';
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface'; import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
export const getJoinColumn = ( export const getJoinColumn = (
@ -9,10 +9,7 @@ export const getJoinColumn = (
relationMetadataArgs: WorkspaceRelationMetadataArgs, relationMetadataArgs: WorkspaceRelationMetadataArgs,
opposite = false, opposite = false,
): string | null => { ): string | null => {
if ( if (relationMetadataArgs.type === RelationType.ONE_TO_MANY) {
relationMetadataArgs.type === RelationMetadataType.ONE_TO_MANY ||
relationMetadataArgs.type === RelationMetadataType.MANY_TO_MANY
) {
return null; return null;
} }
@ -41,7 +38,7 @@ export const getJoinColumn = (
// If we're in a ONE_TO_ONE relation and there are no join columns, we need to find the join column on the inverse side // If we're in a ONE_TO_ONE relation and there are no join columns, we need to find the join column on the inverse side
if ( if (
relationMetadataArgs.type === RelationMetadataType.ONE_TO_ONE && relationMetadataArgs.type === RelationType.ONE_TO_ONE &&
filteredJoinColumnsMetadataArgsCollection.length === 0 && filteredJoinColumnsMetadataArgsCollection.length === 0 &&
!opposite !opposite
) { ) {

View File

@ -4,25 +4,22 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export function isFieldMetadataOfType< export function isFieldMetadataInterfaceOfType<
Field extends FieldMetadataInterface<FieldMetadataType>, Field extends FieldMetadataInterface<FieldMetadataType>,
Type extends FieldMetadataType, Type extends FieldMetadataType,
>( >(
fieldMetadata: Field, fieldMetadata: Field,
type: Type, type: Type,
): fieldMetadata is Field & FieldMetadataInterface<Type>; ): fieldMetadata is Field & FieldMetadataInterface<Type> {
export function isFieldMetadataOfType< return fieldMetadata.type === type;
}
export function isFieldMetadataEntityOfType<
Field extends FieldMetadataEntity<FieldMetadataType>, Field extends FieldMetadataEntity<FieldMetadataType>,
Type extends FieldMetadataType, Type extends FieldMetadataType,
>( >(
fieldMetadata: Field, fieldMetadata: Field,
type: Type, type: Type,
): fieldMetadata is Field & FieldMetadataEntity<Type>; ): fieldMetadata is Field & FieldMetadataEntity<Type> {
export function isFieldMetadataOfType<
Field extends
| FieldMetadataInterface<FieldMetadataType>
| FieldMetadataEntity<FieldMetadataType>,
Type extends FieldMetadataType,
>(fieldMetadata: Field, type: Type): boolean {
return fieldMetadata.type === type; return fieldMetadata.type === type;
} }

View File

@ -93,9 +93,13 @@ export class WorkspaceManagerService {
schemaName, schemaName,
); );
const featureFlags =
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
await this.workspaceSyncMetadataService.synchronize({ await this.workspaceSyncMetadataService.synchronize({
workspaceId, workspaceId,
dataSourceId: dataSourceMetadata.id, dataSourceId: dataSourceMetadata.id,
featureFlags,
}); });
const dataSourceMetadataCreationEnd = performance.now(); const dataSourceMetadataCreationEnd = performance.now();
@ -145,9 +149,13 @@ export class WorkspaceManagerService {
schemaName, schemaName,
); );
const featureFlags =
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
await this.workspaceSyncMetadataService.synchronize({ await this.workspaceSyncMetadataService.synchronize({
workspaceId, workspaceId,
dataSourceId: dataSourceMetadata.id, dataSourceId: dataSourceMetadata.id,
featureFlags,
}); });
await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId); await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId);
@ -165,9 +173,13 @@ export class WorkspaceManagerService {
schemaName, schemaName,
); );
const featureFlags =
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
await this.workspaceSyncMetadataService.synchronize({ await this.workspaceSyncMetadataService.synchronize({
workspaceId: workspaceId, workspaceId: workspaceId,
dataSourceId: dataSourceMetadata.id, dataSourceId: dataSourceMetadata.id,
featureFlags,
}); });
await this.initPermissionsDev(workspaceId); await this.initPermissionsDev(workspaceId);

View File

@ -1,12 +1,14 @@
import { WorkspaceMigrationIndexFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory'; import { WorkspaceMigrationIndexFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory';
import { WorkspaceMigrationObjectFactory } from './workspace-migration-object.factory'; import { WorkspaceMigrationFieldRelationFactory } from './workspace-migration-field-relation.factory';
import { WorkspaceMigrationFieldFactory } from './workspace-migration-field.factory'; import { WorkspaceMigrationFieldFactory } from './workspace-migration-field.factory';
import { WorkspaceMigrationObjectFactory } from './workspace-migration-object.factory';
import { WorkspaceMigrationRelationFactory } from './workspace-migration-relation.factory'; import { WorkspaceMigrationRelationFactory } from './workspace-migration-relation.factory';
export const workspaceMigrationBuilderFactories = [ export const workspaceMigrationBuilderFactories = [
WorkspaceMigrationObjectFactory, WorkspaceMigrationObjectFactory,
WorkspaceMigrationFieldFactory, WorkspaceMigrationFieldFactory,
WorkspaceMigrationFieldRelationFactory,
WorkspaceMigrationRelationFactory, WorkspaceMigrationRelationFactory,
WorkspaceMigrationIndexFactory, WorkspaceMigrationIndexFactory,
]; ];

View File

@ -0,0 +1,388 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationEntity,
WorkspaceMigrationTableAction,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { FieldMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory';
import { camelCase } from 'src/utils/camel-case';
@Injectable()
export class WorkspaceMigrationFieldRelationFactory {
constructor(
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
) {}
async create(
originalObjectMetadataCollection: ObjectMetadataEntity[],
fieldMetadataCollection: FieldMetadataEntity<FieldMetadataType.RELATION>[],
action:
| WorkspaceMigrationBuilderAction.CREATE
| WorkspaceMigrationBuilderAction.DELETE,
): Promise<Partial<WorkspaceMigrationEntity>[]>;
async create(
originalObjectMetadataCollection: ObjectMetadataEntity[],
fieldMetadataUpdateCollection: FieldMetadataUpdate<FieldMetadataType.RELATION>[],
action: WorkspaceMigrationBuilderAction.UPDATE,
): Promise<Partial<WorkspaceMigrationEntity>[]>;
/**
* Deletion of the relation is handled by field deletion
*/
async create(
originalObjectMetadataCollection: ObjectMetadataEntity[],
fieldMetadataCollectionOrFieldMetadataUpdateCollection:
| FieldMetadataEntity<FieldMetadataType.RELATION>[]
| FieldMetadataUpdate<FieldMetadataType.RELATION>[],
action: WorkspaceMigrationBuilderAction,
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const originalObjectMetadataMap = originalObjectMetadataCollection.reduce(
(result, currentObject) => {
result[currentObject.id] = currentObject;
return result;
},
{} as Record<string, ObjectMetadataEntity>,
);
switch (action) {
case WorkspaceMigrationBuilderAction.CREATE:
return this.createFieldRelationMigration(
originalObjectMetadataMap,
fieldMetadataCollectionOrFieldMetadataUpdateCollection as FieldMetadataEntity<FieldMetadataType.RELATION>[],
);
case WorkspaceMigrationBuilderAction.UPDATE:
return this.updateFieldRelationMigration(
originalObjectMetadataMap,
fieldMetadataCollectionOrFieldMetadataUpdateCollection as FieldMetadataUpdate<FieldMetadataType.RELATION>[],
);
case WorkspaceMigrationBuilderAction.DELETE:
return this.deleteFieldRelationMigration(
originalObjectMetadataMap,
fieldMetadataCollectionOrFieldMetadataUpdateCollection as FieldMetadataEntity<FieldMetadataType.RELATION>[],
);
default:
return [];
}
}
private async updateFieldRelationMigration(
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
fieldMetadataUpdateCollection: FieldMetadataUpdate<FieldMetadataType.RELATION>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
for (const {
altered: sourceFieldMetadata,
} of fieldMetadataUpdateCollection) {
const sourceObjectMetadata =
originalObjectMetadataMap[sourceFieldMetadata.objectMetadataId];
const targetObjectMetadata =
originalObjectMetadataMap[
sourceFieldMetadata.relationTargetObjectMetadataId
];
if (!sourceObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${sourceFieldMetadata.objectMetadataId} not found`,
);
}
if (!targetObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${sourceFieldMetadata.relationTargetObjectMetadataId} not found`,
);
}
const targetFieldMetadata = targetObjectMetadata.fields.find(
(field) =>
field.id === sourceFieldMetadata.relationTargetFieldMetadataId,
);
if (!targetFieldMetadata) {
throw new Error(
`FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} not found`,
);
}
if (
!isFieldMetadataEntityOfType(
targetFieldMetadata,
FieldMetadataType.RELATION,
)
) {
throw new Error(
`FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} is not a relation`,
);
}
if (!targetFieldMetadata.settings) {
throw new Error(
`FieldMetadata for relation with id ${sourceFieldMetadata.id} has no settings`,
);
}
const migrations: WorkspaceMigrationTableAction[] = [
{
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP_FOREIGN_KEY,
columnName: `${camelCase(targetFieldMetadata.name)}Id`,
},
],
},
{
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${camelCase(targetFieldMetadata.name)}Id`,
referencedTableName:
computeObjectTargetTable(sourceObjectMetadata),
referencedTableColumnName: 'id',
isUnique:
targetFieldMetadata.settings.relationType ===
RelationType.ONE_TO_ONE,
onDelete: targetFieldMetadata.settings.onDelete,
},
],
},
];
workspaceMigrations.push({
workspaceId: sourceFieldMetadata.workspaceId,
name: generateMigrationName(
`update-relation-from-${sourceObjectMetadata.nameSingular}-to-${targetObjectMetadata.nameSingular}`,
),
isCustom: false,
migrations,
});
}
return workspaceMigrations;
}
private async createFieldRelationMigration(
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
fieldRelationMetadataCollection: FieldMetadataEntity<FieldMetadataType.RELATION>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
for (const sourceFieldMetadata of fieldRelationMetadataCollection) {
const sourceObjectMetadata =
originalObjectMetadataMap[sourceFieldMetadata.objectMetadataId];
const targetObjectMetadata =
originalObjectMetadataMap[
sourceFieldMetadata.relationTargetObjectMetadataId
];
if (!sourceFieldMetadata.settings) {
throw new Error(
`FieldMetadata for relation with id ${sourceFieldMetadata.id} has no settings`,
);
}
// We're creating it from `ONE_TO_MANY` with the join column so we don't need to create a migration for `MANY_TO_ONE`
if (
sourceFieldMetadata.settings.relationType === RelationType.MANY_TO_ONE
) {
continue;
}
if (!sourceObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${sourceFieldMetadata.objectMetadataId} not found`,
);
}
if (!targetObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${sourceFieldMetadata.relationTargetObjectMetadataId} not found`,
);
}
const targetFieldMetadata = targetObjectMetadata.fields.find(
(field) =>
field.id === sourceFieldMetadata.relationTargetFieldMetadataId,
);
if (!targetFieldMetadata) {
throw new Error(
`FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} not found`,
);
}
if (
!isFieldMetadataEntityOfType(
targetFieldMetadata,
FieldMetadataType.RELATION,
)
) {
throw new Error(
`FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} is not a relation`,
);
}
if (!targetFieldMetadata.settings) {
throw new Error(
`FieldMetadata for relation with id ${sourceFieldMetadata.id} has no settings`,
);
}
if (!targetFieldMetadata.settings.joinColumnName) {
continue;
}
const migrations: WorkspaceMigrationTableAction[] = [
{
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
...this.workspaceMigrationFactory.createColumnActions(
WorkspaceMigrationColumnActionType.CREATE,
targetFieldMetadata,
),
],
},
{
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName:
targetFieldMetadata.settings.joinColumnName ??
`${camelCase(targetFieldMetadata.name)}Id`,
referencedTableName:
computeObjectTargetTable(sourceObjectMetadata),
referencedTableColumnName: 'id',
isUnique:
targetFieldMetadata.settings.relationType ===
RelationType.ONE_TO_ONE,
onDelete: targetFieldMetadata.settings.onDelete,
},
],
},
];
workspaceMigrations.push({
workspaceId: sourceFieldMetadata.workspaceId,
name: generateMigrationName(
`create-relation-from-${sourceObjectMetadata.nameSingular}-to-${targetObjectMetadata.nameSingular}`,
),
isCustom: false,
migrations,
});
}
return workspaceMigrations;
}
private async deleteFieldRelationMigration(
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
fieldRelationMetadataCollection: FieldMetadataEntity<FieldMetadataType.RELATION>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
for (const sourceFieldMetadata of fieldRelationMetadataCollection) {
const sourceObjectMetadata =
originalObjectMetadataMap[sourceFieldMetadata.objectMetadataId];
const targetObjectMetadata =
originalObjectMetadataMap[
sourceFieldMetadata.relationTargetObjectMetadataId
];
if (!sourceObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${sourceFieldMetadata.objectMetadataId} not found`,
);
}
if (!targetObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${sourceFieldMetadata.relationTargetObjectMetadataId} not found`,
);
}
const targetFieldMetadata = targetObjectMetadata.fields.find(
(field) =>
field.id === sourceFieldMetadata.relationTargetFieldMetadataId,
);
if (!targetFieldMetadata) {
throw new Error(
`FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} not found`,
);
}
if (
!isFieldMetadataEntityOfType(
targetFieldMetadata,
FieldMetadataType.RELATION,
)
) {
throw new Error(
`FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} is not a relation`,
);
}
if (!targetFieldMetadata.settings) {
throw new Error(
`FieldMetadata for relation with id ${sourceFieldMetadata.id} has no settings`,
);
}
const migrations: WorkspaceMigrationTableAction[] = [
{
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP_FOREIGN_KEY,
columnName: `${camelCase(targetFieldMetadata.name)}Id`,
},
],
},
{
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName:
targetFieldMetadata.settings.joinColumnName ??
`${camelCase(targetFieldMetadata.name)}Id`,
},
],
},
];
workspaceMigrations.push({
workspaceId: sourceFieldMetadata.workspaceId,
name: generateMigrationName(
`update-relation-from-${sourceObjectMetadata.nameSingular}-to-${targetObjectMetadata.nameSingular}`,
),
isCustom: false,
migrations,
});
}
return workspaceMigrations;
}
}

View File

@ -5,6 +5,8 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
@ -18,15 +20,18 @@ import {
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
export interface FieldMetadataUpdate { export interface FieldMetadataUpdate<
current: FieldMetadataEntity; Type extends FieldMetadataType = FieldMetadataType,
altered: FieldMetadataEntity; > {
current: FieldMetadataEntity<Type>;
altered: FieldMetadataEntity<Type>;
} }
@Injectable() @Injectable()
export class WorkspaceMigrationFieldFactory { export class WorkspaceMigrationFieldFactory {
constructor( constructor(
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly featureFlagService: FeatureFlagService,
) {} ) {}
async create( async create(
@ -86,6 +91,17 @@ export class WorkspaceMigrationFieldFactory {
): Promise<Partial<WorkspaceMigrationEntity>[]> { ): Promise<Partial<WorkspaceMigrationEntity>[]> {
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = []; const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
if (fieldMetadataCollection.length === 0) {
return [];
}
const workspaceId = fieldMetadataCollection[0]?.workspaceId;
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
const fieldMetadataCollectionGroupByObjectMetadataId = const fieldMetadataCollectionGroupByObjectMetadataId =
fieldMetadataCollection.reduce( fieldMetadataCollection.reduce(
(result, currentFieldMetadata) => { (result, currentFieldMetadata) => {
@ -110,7 +126,10 @@ export class WorkspaceMigrationFieldFactory {
for (const fieldMetadata of fieldMetadataCollection) { for (const fieldMetadata of fieldMetadataCollection) {
// Relations are handled in workspace-migration-relation.factory.ts // Relations are handled in workspace-migration-relation.factory.ts
if (fieldMetadata.type === FieldMetadataType.RELATION) { if (
!isNewRelationEnabled &&
fieldMetadata.type === FieldMetadataType.RELATION
) {
continue; continue;
} }
@ -127,7 +146,7 @@ export class WorkspaceMigrationFieldFactory {
name: generateMigrationName( name: generateMigrationName(
`create-${objectMetadata.nameSingular}-fields`, `create-${objectMetadata.nameSingular}-fields`,
), ),
isCustom: false, isCustom: objectMetadata.isCustom,
migrations: [ migrations: [
{ {
name: computeObjectTargetTable( name: computeObjectTargetTable(
@ -149,9 +168,23 @@ export class WorkspaceMigrationFieldFactory {
): Promise<Partial<WorkspaceMigrationEntity>[]> { ): Promise<Partial<WorkspaceMigrationEntity>[]> {
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = []; const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
if (fieldMetadataUpdateCollection.length === 0) {
return [];
}
const workspaceId = fieldMetadataUpdateCollection[0]?.current.workspaceId;
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
for (const fieldMetadataUpdate of fieldMetadataUpdateCollection) { for (const fieldMetadataUpdate of fieldMetadataUpdateCollection) {
// Skip relations, because they're just representation and not real columns // Skip relations, because they're just representation and not real columns
if (fieldMetadataUpdate.altered.type === FieldMetadataType.RELATION) { if (
!isNewRelationEnabled &&
fieldMetadataUpdate.altered.type === FieldMetadataType.RELATION
) {
continue; continue;
} }
@ -211,7 +244,7 @@ export class WorkspaceMigrationFieldFactory {
name: generateMigrationName( name: generateMigrationName(
`update-${fieldMetadataUpdate.altered.name}`, `update-${fieldMetadataUpdate.altered.name}`,
), ),
isCustom: false, isCustom: fieldMetadataUpdate.altered.isCustom,
migrations, migrations,
}); });
} }
@ -250,7 +283,7 @@ export class WorkspaceMigrationFieldFactory {
workspaceMigrations.push({ workspaceMigrations.push({
workspaceId: fieldMetadata.workspaceId, workspaceId: fieldMetadata.workspaceId,
name: generateMigrationName(`delete-${fieldMetadata.name}`), name: generateMigrationName(`delete-${fieldMetadata.name}`),
isCustom: false, isCustom: fieldMetadata.isCustom,
migrations, migrations,
}); });
} }

View File

@ -109,7 +109,7 @@ export class WorkspaceMigrationObjectFactory {
workspaceMigrations.push({ workspaceMigrations.push({
workspaceId: objectMetadata.workspaceId, workspaceId: objectMetadata.workspaceId,
name: generateMigrationName(`create-${objectMetadata.nameSingular}`), name: generateMigrationName(`create-${objectMetadata.nameSingular}`),
isCustom: false, isCustom: objectMetadata.isCustom,
migrations, migrations,
}); });
} }
@ -136,7 +136,7 @@ export class WorkspaceMigrationObjectFactory {
name: generateMigrationName( name: generateMigrationName(
`rename-${objectMetadataUpdate.current.nameSingular}`, `rename-${objectMetadataUpdate.current.nameSingular}`,
), ),
isCustom: false, isCustom: objectMetadataUpdate.altered.isCustom,
migrations: [ migrations: [
{ {
name: oldTableName, name: oldTableName,
@ -167,7 +167,7 @@ export class WorkspaceMigrationObjectFactory {
workspaceMigrations.push({ workspaceMigrations.push({
workspaceId: objectMetadata.workspaceId, workspaceId: objectMetadata.workspaceId,
name: generateMigrationName(`delete-${objectMetadata.nameSingular}`), name: generateMigrationName(`delete-${objectMetadata.nameSingular}`),
isCustom: false, isCustom: objectMetadata.isCustom,
migrations: [ migrations: [
...(relationMetadataCollection ?? []).map( ...(relationMetadataCollection ?? []).map(
(relationMetadata) => (relationMetadata) =>

View File

@ -1,11 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { workspaceMigrationBuilderFactories } from './factories'; import { workspaceMigrationBuilderFactories } from './factories';
@Module({ @Module({
imports: [WorkspaceMigrationModule], imports: [WorkspaceMigrationModule, FeatureFlagModule],
providers: [...workspaceMigrationBuilderFactories], providers: [...workspaceMigrationBuilderFactories],
exports: [...workspaceMigrationBuilderFactories], exports: [...workspaceMigrationBuilderFactories],
}) })

View File

@ -3,6 +3,10 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-met
export const convertOnDeleteActionToOnDelete = ( export const convertOnDeleteActionToOnDelete = (
onDeleteAction: RelationOnDeleteAction | undefined, onDeleteAction: RelationOnDeleteAction | undefined,
): 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION' | undefined => { ): 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION' | undefined => {
if (!onDeleteAction) {
return undefined;
}
switch (onDeleteAction) { switch (onDeleteAction) {
case 'CASCADE': case 'CASCADE':
return 'CASCADE'; return 'CASCADE';

View File

@ -7,6 +7,7 @@ import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner, ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs, RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@ -25,6 +26,7 @@ export class SyncWorkspaceMetadataCommand extends ActiveOrSuspendedWorkspacesMig
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService, private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
private readonly dataSourceService: DataSourceService, private readonly dataSourceService: DataSourceService,
private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService, private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService,
private readonly featureFlagService: FeatureFlagService,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) { ) {
super(workspaceRepository, twentyORMGlobalManager); super(workspaceRepository, twentyORMGlobalManager);
@ -45,11 +47,15 @@ export class SyncWorkspaceMetadataCommand extends ActiveOrSuspendedWorkspacesMig
workspaceId, workspaceId,
); );
const featureFlags =
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
const { storage, workspaceMigrations } = const { storage, workspaceMigrations } =
await this.workspaceSyncMetadataService.synchronize( await this.workspaceSyncMetadataService.synchronize(
{ {
workspaceId, workspaceId,
dataSourceId: dataSourceMetadata.id, dataSourceId: dataSourceMetadata.id,
featureFlags,
}, },
{ applyChanges: !options.dryRun }, { applyChanges: !options.dryRun },
); );

View File

@ -1,6 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
@ -19,6 +20,7 @@ import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command'
WorkspaceModule, WorkspaceModule,
DataSourceModule, DataSourceModule,
WorkspaceDataSourceModule, WorkspaceDataSourceModule,
FeatureFlagModule,
TypeOrmModule.forFeature([Workspace], 'core'), TypeOrmModule.forFeature([Workspace], 'core'),
SyncWorkspaceLoggerModule, SyncWorkspaceLoggerModule,
], ],

View File

@ -1,3 +1,4 @@
import { WorkspaceFieldRelationComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator';
import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator'; import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator';
import { WorkspaceFieldComparator } from './workspace-field.comparator'; import { WorkspaceFieldComparator } from './workspace-field.comparator';
@ -6,6 +7,7 @@ import { WorkspaceRelationComparator } from './workspace-relation.comparator';
export const workspaceSyncMetadataComparators = [ export const workspaceSyncMetadataComparators = [
WorkspaceFieldComparator, WorkspaceFieldComparator,
WorkspaceFieldRelationComparator,
WorkspaceObjectComparator, WorkspaceObjectComparator,
WorkspaceRelationComparator, WorkspaceRelationComparator,
WorkspaceIndexComparator, WorkspaceIndexComparator,

Some files were not shown because too many files have changed in this diff Show More