Optimistic rendering of morph relations (#14997)
The optimistic rendering of a morph relation Is broken. We intend to fix it here. - mapping of gql fields Fixes https://github.com/twentyhq/core-team-issues/issues/1322 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+331
-122
@@ -3,17 +3,26 @@ import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effec
|
||||
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
||||
import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach';
|
||||
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
|
||||
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { type ApolloCache } from '@apollo/client';
|
||||
import { isArray } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
computeMorphRelationFieldName,
|
||||
CustomError,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
type triggerUpdateRelationsOptimisticEffectArgs = {
|
||||
type TriggerUpdateRelationsOptimisticEffectArgs = {
|
||||
cache: ApolloCache<unknown>;
|
||||
sourceObjectMetadataItem: ObjectMetadataItem;
|
||||
currentSourceRecord: RecordGqlNode | null;
|
||||
@@ -26,129 +35,329 @@ export const triggerUpdateRelationsOptimisticEffect = ({
|
||||
currentSourceRecord,
|
||||
updatedSourceRecord,
|
||||
objectMetadataItems,
|
||||
}: triggerUpdateRelationsOptimisticEffectArgs) => {
|
||||
}: TriggerUpdateRelationsOptimisticEffectArgs) => {
|
||||
const isDeletion =
|
||||
isDefined(updatedSourceRecord) &&
|
||||
isDefined(updatedSourceRecord['deletedAt']);
|
||||
|
||||
return sourceObjectMetadataItem.fields.forEach(
|
||||
(fieldMetadataItemOnSourceRecord) => {
|
||||
const notARelationField =
|
||||
fieldMetadataItemOnSourceRecord.type !== FieldMetadataType.RELATION;
|
||||
sourceObjectMetadataItem.fields.forEach((fieldMetadataItemOnSourceRecord) => {
|
||||
if (
|
||||
!isFieldMorphRelation(fieldMetadataItemOnSourceRecord) &&
|
||||
!isFieldRelation(fieldMetadataItemOnSourceRecord)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (notARelationField) {
|
||||
return;
|
||||
}
|
||||
if (isFieldRelation(fieldMetadataItemOnSourceRecord)) {
|
||||
triggerUpdateRelationOptimisticEffect({
|
||||
fieldMetadataItemOnSourceRecord,
|
||||
updatedSourceRecord,
|
||||
currentSourceRecord,
|
||||
objectMetadataItems,
|
||||
sourceObjectMetadataItem,
|
||||
cache,
|
||||
isDeletion,
|
||||
});
|
||||
}
|
||||
|
||||
const fieldDoesNotExist =
|
||||
isDefined(updatedSourceRecord) &&
|
||||
!(fieldMetadataItemOnSourceRecord.name in updatedSourceRecord);
|
||||
|
||||
if (fieldDoesNotExist) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relation = fieldMetadataItemOnSourceRecord.relation;
|
||||
|
||||
if (!relation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { targetObjectMetadata, targetFieldMetadata } = relation;
|
||||
|
||||
const fullTargetObjectMetadataItem = objectMetadataItems.find(
|
||||
({ nameSingular }) =>
|
||||
nameSingular === targetObjectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
if (!fullTargetObjectMetadataItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlNode
|
||||
| null = currentSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
|
||||
|
||||
const updatedFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlNode
|
||||
| null = updatedSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
|
||||
|
||||
const noDiff = isDeeplyEqual(
|
||||
currentFieldValueOnSourceRecord,
|
||||
updatedFieldValueOnSourceRecord,
|
||||
{ strict: true },
|
||||
);
|
||||
if (noDiff && !isDeletion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extractTargetRecordsFromRelation = (
|
||||
value: RecordGqlConnection | RecordGqlNode | null,
|
||||
): RecordGqlNode[] => {
|
||||
// TODO investigate on the root cause of array injection here, should never occurs
|
||||
// Cache might be corrupted somewhere due to ObjectRecord and RecordGqlNode inclusion
|
||||
if (!isDefined(value) || isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isObjectRecordConnection(relation, value)) {
|
||||
return value.edges.map(({ node }) => node);
|
||||
}
|
||||
|
||||
return [value];
|
||||
};
|
||||
|
||||
const recordToExtractDetachFrom = isDeletion
|
||||
? updatedFieldValueOnSourceRecord
|
||||
: currentFieldValueOnSourceRecord;
|
||||
const targetRecordsToDetachFrom = extractTargetRecordsFromRelation(
|
||||
recordToExtractDetachFrom,
|
||||
);
|
||||
|
||||
// TODO: see if we can de-hardcode this, put cascade delete in relation metadata item
|
||||
// Instead of hardcoding it here
|
||||
const shouldCascadeDeleteTargetRecords =
|
||||
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
|
||||
targetObjectMetadata.nameSingular as CoreObjectNameSingular,
|
||||
);
|
||||
if (shouldCascadeDeleteTargetRecords) {
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem: fullTargetObjectMetadataItem,
|
||||
recordsToDestroy: targetRecordsToDetachFrom,
|
||||
objectMetadataItems,
|
||||
});
|
||||
} else if (isDefined(currentSourceRecord)) {
|
||||
targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
|
||||
triggerDetachRelationOptimisticEffect({
|
||||
cache,
|
||||
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
|
||||
sourceRecordId: currentSourceRecord.id,
|
||||
fieldNameOnTargetRecord: targetFieldMetadata.name,
|
||||
targetObjectNameSingular: targetObjectMetadata.nameSingular,
|
||||
targetRecordId: targetRecordToDetachFrom.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!isDeletion && isDefined(updatedSourceRecord)) {
|
||||
const targetRecordsToAttachTo = extractTargetRecordsFromRelation(
|
||||
updatedFieldValueOnSourceRecord,
|
||||
);
|
||||
|
||||
targetRecordsToAttachTo.forEach((targetRecordToAttachTo) =>
|
||||
triggerAttachRelationOptimisticEffect({
|
||||
cache,
|
||||
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
|
||||
sourceRecordId: updatedSourceRecord.id,
|
||||
fieldNameOnTargetRecord: targetFieldMetadata.name,
|
||||
targetObjectNameSingular: targetObjectMetadata.nameSingular,
|
||||
targetRecordId: targetRecordToAttachTo.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
if (isFieldMorphRelation(fieldMetadataItemOnSourceRecord)) {
|
||||
triggerUpdateMorphRelationOptimisticEffect({
|
||||
fieldMetadataItemOnSourceRecord,
|
||||
updatedSourceRecord,
|
||||
currentSourceRecord,
|
||||
objectMetadataItems,
|
||||
sourceObjectMetadataItem,
|
||||
cache,
|
||||
isDeletion,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const triggerUpdateRelationOptimisticEffect = ({
|
||||
fieldMetadataItemOnSourceRecord,
|
||||
updatedSourceRecord,
|
||||
currentSourceRecord,
|
||||
objectMetadataItems,
|
||||
sourceObjectMetadataItem,
|
||||
cache,
|
||||
isDeletion,
|
||||
}: {
|
||||
fieldMetadataItemOnSourceRecord: FieldMetadataItem;
|
||||
updatedSourceRecord: RecordGqlNode | null;
|
||||
currentSourceRecord: RecordGqlNode | null;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
sourceObjectMetadataItem: ObjectMetadataItem;
|
||||
cache: ApolloCache<unknown>;
|
||||
isDeletion: boolean;
|
||||
}) => {
|
||||
const fieldDoesNotExist =
|
||||
isDefined(updatedSourceRecord) &&
|
||||
!(fieldMetadataItemOnSourceRecord.name in updatedSourceRecord);
|
||||
|
||||
if (fieldDoesNotExist) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relation = fieldMetadataItemOnSourceRecord.relation;
|
||||
if (!relation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { targetObjectMetadata, targetFieldMetadata } = relation;
|
||||
|
||||
const { fieldMetadataItem: targetFieldMetadataFullObject } =
|
||||
getFieldMetadataItemById({
|
||||
fieldMetadataId: targetFieldMetadata.id,
|
||||
objectMetadataItems,
|
||||
});
|
||||
if (!targetFieldMetadataFullObject) {
|
||||
throw new CustomError(
|
||||
'Target field metadata full object not found',
|
||||
'TARGET_FIELD_METADATA_FULL_OBJECT_NOT_FOUND',
|
||||
);
|
||||
}
|
||||
|
||||
const fullTargetObjectMetadataItem = objectMetadataItems.find(
|
||||
({ nameSingular }) => nameSingular === targetObjectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
if (!fullTargetObjectMetadataItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlNode
|
||||
| null = currentSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
|
||||
|
||||
const updatedFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlNode
|
||||
| null = updatedSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
|
||||
|
||||
const noDiff = isDeeplyEqual(
|
||||
currentFieldValueOnSourceRecord,
|
||||
updatedFieldValueOnSourceRecord,
|
||||
{ strict: true },
|
||||
);
|
||||
if (noDiff && !isDeletion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recordToExtractDetachFrom = isDeletion
|
||||
? updatedFieldValueOnSourceRecord
|
||||
: currentFieldValueOnSourceRecord;
|
||||
const targetRecordsToDetachFrom = extractTargetRecordsFromRelation(
|
||||
recordToExtractDetachFrom,
|
||||
relation,
|
||||
);
|
||||
|
||||
// TODO: see if we can de-hardcode this, put cascade delete in relation metadata item
|
||||
// Instead of hardcoding it here
|
||||
const shouldCascadeDeleteTargetRecords =
|
||||
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
|
||||
targetObjectMetadata.nameSingular as CoreObjectNameSingular,
|
||||
);
|
||||
const gqlFieldNameOnTargetRecord =
|
||||
targetFieldMetadataFullObject.type === FieldMetadataType.RELATION
|
||||
? targetFieldMetadataFullObject.name
|
||||
: computeMorphRelationFieldName({
|
||||
fieldName: targetFieldMetadataFullObject.name,
|
||||
relationType: targetFieldMetadataFullObject.settings?.relationType,
|
||||
targetObjectMetadataNameSingular:
|
||||
sourceObjectMetadataItem.nameSingular,
|
||||
targetObjectMetadataNamePlural: sourceObjectMetadataItem.namePlural,
|
||||
});
|
||||
if (shouldCascadeDeleteTargetRecords) {
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem: fullTargetObjectMetadataItem,
|
||||
recordsToDestroy: targetRecordsToDetachFrom,
|
||||
objectMetadataItems,
|
||||
});
|
||||
} else if (isDefined(currentSourceRecord)) {
|
||||
targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
|
||||
triggerDetachRelationOptimisticEffect({
|
||||
cache,
|
||||
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
|
||||
sourceRecordId: currentSourceRecord.id,
|
||||
fieldNameOnTargetRecord: gqlFieldNameOnTargetRecord,
|
||||
targetObjectNameSingular: targetObjectMetadata.nameSingular,
|
||||
targetRecordId: targetRecordToDetachFrom.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!isDeletion && isDefined(updatedSourceRecord)) {
|
||||
const targetRecordsToAttachTo = extractTargetRecordsFromRelation(
|
||||
updatedFieldValueOnSourceRecord,
|
||||
relation,
|
||||
);
|
||||
|
||||
targetRecordsToAttachTo.forEach((targetRecordToAttachTo) =>
|
||||
triggerAttachRelationOptimisticEffect({
|
||||
cache,
|
||||
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
|
||||
sourceRecordId: updatedSourceRecord.id,
|
||||
fieldNameOnTargetRecord: gqlFieldNameOnTargetRecord,
|
||||
targetObjectNameSingular: targetObjectMetadata.nameSingular,
|
||||
targetRecordId: targetRecordToAttachTo.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const triggerUpdateMorphRelationOptimisticEffect = ({
|
||||
fieldMetadataItemOnSourceRecord,
|
||||
updatedSourceRecord,
|
||||
currentSourceRecord,
|
||||
objectMetadataItems,
|
||||
sourceObjectMetadataItem,
|
||||
cache,
|
||||
isDeletion,
|
||||
}: {
|
||||
fieldMetadataItemOnSourceRecord: FieldMetadataItem;
|
||||
updatedSourceRecord: RecordGqlNode | null;
|
||||
currentSourceRecord: RecordGqlNode | null;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
sourceObjectMetadataItem: ObjectMetadataItem;
|
||||
cache: ApolloCache<unknown>;
|
||||
isDeletion: boolean;
|
||||
}) => {
|
||||
const morphRelations = fieldMetadataItemOnSourceRecord.morphRelations;
|
||||
if (!morphRelations) {
|
||||
return;
|
||||
}
|
||||
|
||||
morphRelations.forEach((morphRelation) => {
|
||||
const gqlFieldMorphRelation = computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadataItemOnSourceRecord.name,
|
||||
relationType: morphRelation.type,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
|
||||
const { fieldMetadataItem: targetFieldMetadataFullObject } =
|
||||
getFieldMetadataItemById({
|
||||
fieldMetadataId: morphRelation.targetFieldMetadata.id,
|
||||
objectMetadataItems,
|
||||
});
|
||||
if (!targetFieldMetadataFullObject) {
|
||||
throw new CustomError(
|
||||
'Target field metadata full object not found',
|
||||
'TARGET_FIELD_METADATA_FULL_OBJECT_NOT_FOUND',
|
||||
);
|
||||
}
|
||||
|
||||
const fieldDoesNotExist =
|
||||
isDefined(updatedSourceRecord) &&
|
||||
!(gqlFieldMorphRelation in updatedSourceRecord);
|
||||
|
||||
if (fieldDoesNotExist) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { targetObjectMetadata, targetFieldMetadata } = morphRelation;
|
||||
|
||||
const fullTargetObjectMetadataItem = objectMetadataItems.find(
|
||||
({ nameSingular }) => nameSingular === targetObjectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
if (!fullTargetObjectMetadataItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlNode
|
||||
| null = currentSourceRecord?.[gqlFieldMorphRelation];
|
||||
|
||||
const updatedFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlNode
|
||||
| null = updatedSourceRecord?.[gqlFieldMorphRelation];
|
||||
|
||||
const noDiff = isDeeplyEqual(
|
||||
currentFieldValueOnSourceRecord,
|
||||
updatedFieldValueOnSourceRecord,
|
||||
{ strict: true },
|
||||
);
|
||||
if (noDiff && !isDeletion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recordToExtractDetachFrom = isDeletion
|
||||
? updatedFieldValueOnSourceRecord
|
||||
: currentFieldValueOnSourceRecord;
|
||||
const targetRecordsToDetachFrom = extractTargetRecordsFromRelation(
|
||||
recordToExtractDetachFrom,
|
||||
morphRelation,
|
||||
);
|
||||
|
||||
// TODO: see if we can de-hardcode this, put cascade delete in relation metadata item
|
||||
// Instead of hardcoding it here
|
||||
const shouldCascadeDeleteTargetRecords =
|
||||
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
|
||||
targetObjectMetadata.nameSingular as CoreObjectNameSingular,
|
||||
);
|
||||
if (shouldCascadeDeleteTargetRecords) {
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem: fullTargetObjectMetadataItem,
|
||||
recordsToDestroy: targetRecordsToDetachFrom,
|
||||
objectMetadataItems,
|
||||
});
|
||||
} else if (isDefined(currentSourceRecord)) {
|
||||
targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
|
||||
triggerDetachRelationOptimisticEffect({
|
||||
cache,
|
||||
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
|
||||
sourceRecordId: currentSourceRecord.id,
|
||||
fieldNameOnTargetRecord: targetFieldMetadata.name,
|
||||
targetObjectNameSingular: targetObjectMetadata.nameSingular,
|
||||
targetRecordId: targetRecordToDetachFrom.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!isDeletion && isDefined(updatedSourceRecord)) {
|
||||
const targetRecordsToAttachTo = extractTargetRecordsFromRelation(
|
||||
updatedFieldValueOnSourceRecord,
|
||||
morphRelation,
|
||||
);
|
||||
|
||||
targetRecordsToAttachTo.forEach((targetRecordToAttachTo) =>
|
||||
triggerAttachRelationOptimisticEffect({
|
||||
cache,
|
||||
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
|
||||
sourceRecordId: updatedSourceRecord.id,
|
||||
fieldNameOnTargetRecord: targetFieldMetadata.name,
|
||||
targetObjectNameSingular: targetObjectMetadata.nameSingular,
|
||||
targetRecordId: targetRecordToAttachTo.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const extractTargetRecordsFromRelation = (
|
||||
value: RecordGqlConnection | RecordGqlNode | null,
|
||||
relation: FieldMetadataItemRelation,
|
||||
): RecordGqlNode[] => {
|
||||
// TODO investigate on the root cause of array injection here, should never occurs
|
||||
// Cache might be corrupted somewhere due to ObjectRecord and RecordGqlNode inclusion
|
||||
if (!isDefined(value) || isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
if (!isDefined(relation)) {
|
||||
throw new Error('Relation found is undefined');
|
||||
}
|
||||
if (isObjectRecordConnection(relation, value)) {
|
||||
return value.edges.map(({ node }) => node);
|
||||
}
|
||||
|
||||
return [value];
|
||||
};
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
}
|
||||
|
||||
if (fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE) {
|
||||
if (gqlField === fieldMetadata.settings?.joinColumnName) {
|
||||
if (gqlField === `${relationFieldName}Id`) {
|
||||
gqlMorphField += `${gqlField}
|
||||
`;
|
||||
continue;
|
||||
|
||||
+2
-2
@@ -71,14 +71,14 @@ export const mapObjectMetadataToGraphQLQuery = ({
|
||||
}
|
||||
|
||||
return fieldMetadata.morphRelations.map((morphRelation) => ({
|
||||
gqlField: computeMorphRelationFieldName({
|
||||
gqlField: `${computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.name,
|
||||
relationType: morphRelation.type,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
}),
|
||||
})}Id`,
|
||||
fieldMetadata: fieldMetadata,
|
||||
}));
|
||||
});
|
||||
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { getFieldMetadataFromGqlField } from '../getFieldMetadataFromGqlField';
|
||||
|
||||
const objectMetadataItemGenerator = (relationType: RelationType) => {
|
||||
return {
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [
|
||||
{
|
||||
name: 'owner',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
morphRelations: [
|
||||
{
|
||||
type: relationType,
|
||||
targetObjectMetadata: {
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
},
|
||||
targetFieldMetadata: {
|
||||
name: 'ownedBy',
|
||||
type: FieldMetadataType.RELATION,
|
||||
},
|
||||
sourceFieldMetadata: {
|
||||
name: 'owner',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
},
|
||||
sourceObjectMetadata: {
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: relationType,
|
||||
targetObjectMetadata: {
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
targetFieldMetadata: {
|
||||
name: 'ownedBy',
|
||||
type: FieldMetadataType.RELATION,
|
||||
},
|
||||
sourceFieldMetadata: {
|
||||
name: 'owner',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
},
|
||||
sourceObjectMetadata: {
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as FieldMetadataItem,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
describe('getFieldMetadataFromGqlField', () => {
|
||||
it('should find "name"', () => {
|
||||
const objectMetadataItem = {
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
} as unknown as FieldMetadataItem,
|
||||
],
|
||||
};
|
||||
|
||||
const result = getFieldMetadataFromGqlField({
|
||||
objectMetadataItem,
|
||||
gqlField: 'name',
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.name).toBe('name');
|
||||
expect(result?.type).toBe(FieldMetadataType.FULL_NAME);
|
||||
});
|
||||
|
||||
it('should find company by join column name of a relation field', () => {
|
||||
const objectMetadataItem = {
|
||||
fields: [
|
||||
{
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
settings: {
|
||||
joinColumnName: 'companyId',
|
||||
},
|
||||
} as FieldMetadataItem,
|
||||
],
|
||||
};
|
||||
|
||||
const result = getFieldMetadataFromGqlField({
|
||||
objectMetadataItem,
|
||||
gqlField: 'companyId',
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.name).toBe('company');
|
||||
expect(result?.type).toBe(FieldMetadataType.RELATION);
|
||||
expect(result?.settings?.joinColumnName).toBe('companyId');
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent field', () => {
|
||||
const objectMetadataItem = {
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
} as unknown as FieldMetadataItem,
|
||||
],
|
||||
};
|
||||
|
||||
const result = getFieldMetadataFromGqlField({
|
||||
objectMetadataItem,
|
||||
gqlField: 'nonExistentField',
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should find owner by direct morph relation ONE_TO_MANY field name', () => {
|
||||
const objectMetadataItem = objectMetadataItemGenerator(
|
||||
RelationType.ONE_TO_MANY,
|
||||
);
|
||||
const result = getFieldMetadataFromGqlField({
|
||||
objectMetadataItem,
|
||||
gqlField: 'owner',
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.name).toBe('owner');
|
||||
expect(result?.type).toBe(FieldMetadataType.MORPH_RELATION);
|
||||
});
|
||||
|
||||
it('should find ownerCompanies by computed morph relation ONE_TO_MANY field name', () => {
|
||||
const objectMetadataItem = objectMetadataItemGenerator(
|
||||
RelationType.ONE_TO_MANY,
|
||||
);
|
||||
const result = getFieldMetadataFromGqlField({
|
||||
objectMetadataItem,
|
||||
gqlField: 'ownerCompanies',
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.name).toBe('owner');
|
||||
expect(result?.type).toBe(FieldMetadataType.MORPH_RELATION);
|
||||
});
|
||||
|
||||
it('should find ownerPerson by computed morph relation MANY_TO_ONE field name', () => {
|
||||
const objectMetadataItem = objectMetadataItemGenerator(
|
||||
RelationType.MANY_TO_ONE,
|
||||
);
|
||||
const result = getFieldMetadataFromGqlField({
|
||||
objectMetadataItem,
|
||||
gqlField: 'ownerPerson',
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.name).toBe('owner');
|
||||
expect(result?.type).toBe(FieldMetadataType.MORPH_RELATION);
|
||||
});
|
||||
|
||||
it('should find ownerPersonId by computed morph relation join column name MANY_TO_ONE field name', () => {
|
||||
const objectMetadataItem = objectMetadataItemGenerator(
|
||||
RelationType.MANY_TO_ONE,
|
||||
);
|
||||
const result = getFieldMetadataFromGqlField({
|
||||
objectMetadataItem,
|
||||
gqlField: 'ownerPersonId',
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.name).toBe('owner');
|
||||
expect(result?.type).toBe(FieldMetadataType.MORPH_RELATION);
|
||||
});
|
||||
});
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { type FieldMorphRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
|
||||
export const computePossibleMorphGqlFieldForFieldName = ({
|
||||
fieldMetadata,
|
||||
}: {
|
||||
fieldMetadata: Pick<
|
||||
FieldMorphRelationMetadata,
|
||||
'morphRelations' | 'fieldName'
|
||||
>;
|
||||
}) =>
|
||||
fieldMetadata.morphRelations.map((morphRelation) => {
|
||||
return computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.fieldName,
|
||||
relationType: morphRelation.type,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
});
|
||||
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { computePossibleMorphGqlFieldForFieldName } from '@/object-record/cache/utils/computePossibleMorphGqlFieldForFieldName';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
|
||||
export const getFieldMetadataFromGqlField = ({
|
||||
objectMetadataItem,
|
||||
gqlField,
|
||||
}: {
|
||||
objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>;
|
||||
gqlField: string;
|
||||
}) => {
|
||||
return (
|
||||
objectMetadataItem.fields.find((field) => field.name === gqlField) ??
|
||||
objectMetadataItem.fields.find(
|
||||
(field) => field.settings?.joinColumnName === gqlField,
|
||||
) ??
|
||||
objectMetadataItem.fields.filter(isFieldMorphRelation).find((field) => {
|
||||
const morphRelations = field.morphRelations;
|
||||
if (!morphRelations) return false;
|
||||
const possibleMorphRelationsNames =
|
||||
computePossibleMorphGqlFieldForFieldName({
|
||||
fieldMetadata: {
|
||||
morphRelations: field.morphRelations ?? [],
|
||||
fieldName: field.name,
|
||||
},
|
||||
});
|
||||
return possibleMorphRelationsNames
|
||||
.flatMap((possibleMorphRelationName) => [
|
||||
possibleMorphRelationName,
|
||||
`${possibleMorphRelationName}Id`,
|
||||
])
|
||||
.includes(gqlField);
|
||||
})
|
||||
);
|
||||
};
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
|
||||
|
||||
import { type FieldMorphRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
|
||||
|
||||
export const getFieldMetadataMorphRelationFromGqlField = ({
|
||||
objectMetadataItems,
|
||||
fieldMetadata,
|
||||
gqlField,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
fieldMetadata: Pick<FieldMorphRelationMetadata, 'morphRelations'>;
|
||||
gqlField: string;
|
||||
}) => {
|
||||
const possibleMorphRelationGqlFields = fieldMetadata.morphRelations.map(
|
||||
(morphRelation) => {
|
||||
const targetObjectMetadata = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id === morphRelation.targetObjectMetadata.id,
|
||||
);
|
||||
if (!targetObjectMetadata) {
|
||||
return {
|
||||
gqlField: undefined,
|
||||
morphRelation: undefined,
|
||||
};
|
||||
}
|
||||
const computedName = computeMorphRelationFieldName({
|
||||
fieldName: morphRelation.sourceFieldMetadata.name,
|
||||
relationType: morphRelation.type,
|
||||
targetObjectMetadataNameSingular: targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural: targetObjectMetadata.namePlural,
|
||||
});
|
||||
|
||||
return {
|
||||
gqlField: computedName,
|
||||
morphRelation,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const gqlFieldMorphRelation = possibleMorphRelationGqlFields.find(
|
||||
(possibeMorphRelationGqlField) =>
|
||||
possibeMorphRelationGqlField.gqlField === gqlField,
|
||||
);
|
||||
|
||||
if (!gqlFieldMorphRelation || !gqlFieldMorphRelation.morphRelation) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
morphRelation: gqlFieldMorphRelation.morphRelation,
|
||||
sourceFieldMetadata: getFieldMetadataItemById({
|
||||
fieldMetadataId:
|
||||
gqlFieldMorphRelation.morphRelation.sourceFieldMetadata.id,
|
||||
objectMetadataItems,
|
||||
}),
|
||||
targetFieldMetadata: getFieldMetadataItemById({
|
||||
fieldMetadataId:
|
||||
gqlFieldMorphRelation.morphRelation?.targetFieldMetadata.id,
|
||||
objectMetadataItems,
|
||||
}),
|
||||
targetObjectMetadata: objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id ===
|
||||
gqlFieldMorphRelation.morphRelation?.targetObjectMetadata.id,
|
||||
),
|
||||
sourceObjectMetadata: objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id ===
|
||||
gqlFieldMorphRelation.morphRelation?.sourceObjectMetadata.id,
|
||||
),
|
||||
};
|
||||
};
|
||||
+5
-5
@@ -1,6 +1,7 @@
|
||||
import { isNull, isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getFieldMetadataFromGqlField } from '@/object-record/cache/utils/getFieldMetadataFromGqlField';
|
||||
import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename';
|
||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
|
||||
@@ -48,11 +49,10 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const field =
|
||||
objectMetadataItem.fields.find((field) => field.name === fieldName) ??
|
||||
objectMetadataItem.fields.find(
|
||||
(field) => field.settings?.joinColumnName === fieldName,
|
||||
);
|
||||
const field = getFieldMetadataFromGqlField({
|
||||
objectMetadataItem,
|
||||
gqlField: fieldName,
|
||||
});
|
||||
|
||||
if (isUndefined(field)) {
|
||||
return undefined;
|
||||
|
||||
+169
-68
@@ -2,6 +2,8 @@ import { isNull, isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getFieldMetadataFromGqlField } from '@/object-record/cache/utils/getFieldMetadataFromGqlField';
|
||||
import { getFieldMetadataMorphRelationFromGqlField } from '@/object-record/cache/utils/getFieldMetadataMorphRelationFromGqlField';
|
||||
import {
|
||||
getRecordFromCache,
|
||||
type GetRecordFromCacheArgs,
|
||||
@@ -51,16 +53,9 @@ export const computeOptimisticRecordFromInput = ({
|
||||
objectMetadataItem.fields.find((field) => {
|
||||
if (!isFieldMorphRelation(field)) return false;
|
||||
|
||||
return field.morphRelations?.some((morphRelation) => {
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName: field.name,
|
||||
relationType: morphRelation.type,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
return computedFieldName === recordKey.replace('Id', '');
|
||||
return getFieldMetadataFromGqlField({
|
||||
objectMetadataItem,
|
||||
gqlField: recordKey,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,7 +115,8 @@ export const computeOptimisticRecordFromInput = ({
|
||||
}
|
||||
|
||||
const isRelationField = isFieldRelation(fieldMetadataItem);
|
||||
if (!isRelationField) {
|
||||
const isMorphRelationField = isFieldMorphRelation(fieldMetadataItem);
|
||||
if (!isRelationField && !isMorphRelationField) {
|
||||
if (!isDefined(recordInputFieldValue)) {
|
||||
continue;
|
||||
}
|
||||
@@ -133,78 +129,183 @@ export const computeOptimisticRecordFromInput = ({
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldMetadataItem.relation?.type === RelationType.ONE_TO_MANY) {
|
||||
continue;
|
||||
}
|
||||
if (isRelationField) {
|
||||
if (fieldMetadataItem.relation?.type === RelationType.ONE_TO_MANY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isManyToOneRelation =
|
||||
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE;
|
||||
if (!isManyToOneRelation) {
|
||||
continue;
|
||||
}
|
||||
const isManyToOneRelation =
|
||||
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE;
|
||||
if (!isManyToOneRelation) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isUndefined(recordInputFieldValue)) {
|
||||
throw new Error(
|
||||
`Should never provide relation mutation through anything else than the fieldId e.g companyId and not company, encountered: ${fieldMetadataItem.name}`,
|
||||
if (!isUndefined(recordInputFieldValue)) {
|
||||
throw new Error(
|
||||
`Should never provide relation mutation through anything else than the fieldId e.g companyId and not company, encountered: ${fieldMetadataItem.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationFieldIdName = getForeignKeyNameFromRelationFieldName(
|
||||
fieldMetadataItem.name,
|
||||
);
|
||||
}
|
||||
|
||||
const relationFieldIdName = getForeignKeyNameFromRelationFieldName(
|
||||
fieldMetadataItem.name,
|
||||
);
|
||||
const recordInputFieldIdValue: string | null | undefined =
|
||||
recordInput[relationFieldIdName];
|
||||
|
||||
const recordInputFieldIdValue: string | null | undefined =
|
||||
recordInput[relationFieldIdName];
|
||||
if (isUndefined(recordInputFieldIdValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isUndefined(recordInputFieldIdValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) => field.name === relationFieldIdName,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(relationIdFieldMetadataItem) &&
|
||||
!isDefined(fieldMetadataItem.settings?.joinColumnName)
|
||||
) {
|
||||
throw new Error(
|
||||
'Should never occur, encountered unknown relationId within relations definitions',
|
||||
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) => field.name === relationFieldIdName,
|
||||
);
|
||||
}
|
||||
|
||||
if (isNull(recordInputFieldIdValue)) {
|
||||
optimisticRecord[relationFieldIdName] = null;
|
||||
optimisticRecord[fieldMetadataItem.name] = null;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!isDefined(relationIdFieldMetadataItem) &&
|
||||
!isDefined(fieldMetadataItem.settings?.joinColumnName)
|
||||
) {
|
||||
throw new Error(
|
||||
'Should never occur, encountered unknown relationId within relations definitions',
|
||||
);
|
||||
}
|
||||
|
||||
const targetNameSingular =
|
||||
fieldMetadataItem.relation?.targetObjectMetadata.nameSingular;
|
||||
const targetObjectMetataDataItem = objectMetadataItems.find(
|
||||
({ nameSingular }) => nameSingular === targetNameSingular,
|
||||
);
|
||||
if (!isDefined(targetObjectMetataDataItem)) {
|
||||
throw new Error(
|
||||
'Should never occur, encountered invalid relation definition',
|
||||
if (isNull(recordInputFieldIdValue)) {
|
||||
optimisticRecord[relationFieldIdName] = null;
|
||||
optimisticRecord[fieldMetadataItem.name] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetNameSingular =
|
||||
fieldMetadataItem.relation?.targetObjectMetadata.nameSingular;
|
||||
const targetObjectMetataDataItem = objectMetadataItems.find(
|
||||
({ nameSingular }) => nameSingular === targetNameSingular,
|
||||
);
|
||||
if (!isDefined(targetObjectMetataDataItem)) {
|
||||
throw new Error(
|
||||
'Should never occur, encountered invalid relation definition',
|
||||
);
|
||||
}
|
||||
|
||||
const cachedRecord = getRecordFromCache({
|
||||
cache,
|
||||
objectMetadataItem: targetObjectMetataDataItem,
|
||||
objectMetadataItems,
|
||||
recordId: recordInputFieldIdValue as string,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
});
|
||||
|
||||
optimisticRecord[relationFieldIdName] = recordInputFieldIdValue;
|
||||
|
||||
if (!isDefined(cachedRecord) || Object.keys(cachedRecord).length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
optimisticRecord[fieldMetadataItem.name] = cachedRecord;
|
||||
}
|
||||
|
||||
const cachedRecord = getRecordFromCache({
|
||||
cache,
|
||||
objectMetadataItem: targetObjectMetataDataItem,
|
||||
objectMetadataItems,
|
||||
recordId: recordInputFieldIdValue as string,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
});
|
||||
if (isMorphRelationField) {
|
||||
const relationType = fieldMetadataItem.settings?.relationType;
|
||||
if (relationType === RelationType.ONE_TO_MANY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
optimisticRecord[relationFieldIdName] = recordInputFieldIdValue;
|
||||
const isManyToOneRelation = relationType === RelationType.MANY_TO_ONE;
|
||||
if (!isManyToOneRelation) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isDefined(cachedRecord) || Object.keys(cachedRecord).length <= 0) {
|
||||
continue;
|
||||
if (!isUndefined(recordInputFieldValue)) {
|
||||
throw new Error(
|
||||
`Should never provide relation mutation through anything else than the fieldId e.g companyId and not company, encountered: ${fieldMetadataItem.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationFieldIdNames = fieldMetadataItem.morphRelations?.map(
|
||||
(morphRelation) => {
|
||||
return computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadataItem.name,
|
||||
relationType,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const relationFieldName = relationFieldIdNames?.find(
|
||||
(relationFieldIdName) => recordInput[`${relationFieldIdName}Id`],
|
||||
);
|
||||
|
||||
const relationFieldIdName = `${relationFieldName}Id`;
|
||||
|
||||
if (isUndefined(relationFieldName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const recordInputFieldIdValue: string | null | undefined =
|
||||
recordInput[relationFieldIdName];
|
||||
|
||||
if (isUndefined(recordInputFieldIdValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relationIdFieldMetadataItem = getFieldMetadataFromGqlField({
|
||||
objectMetadataItem,
|
||||
gqlField: relationFieldName,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(relationIdFieldMetadataItem) &&
|
||||
!isDefined(fieldMetadataItem.settings?.joinColumnName)
|
||||
) {
|
||||
throw new Error(
|
||||
'Should never occur, encountered unknown relationId within relations definitions',
|
||||
);
|
||||
}
|
||||
|
||||
if (isNull(recordInputFieldIdValue)) {
|
||||
optimisticRecord[relationFieldName] = null;
|
||||
optimisticRecord[fieldMetadataItem.name] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isDefined(fieldMetadataItem.morphRelations)) {
|
||||
throw new Error(
|
||||
'Should never occur, encountered invalid relation definition',
|
||||
);
|
||||
}
|
||||
|
||||
const fieldMetadataMorphRelation =
|
||||
getFieldMetadataMorphRelationFromGqlField({
|
||||
objectMetadataItems,
|
||||
fieldMetadata: { morphRelations: fieldMetadataItem.morphRelations },
|
||||
gqlField: relationFieldName,
|
||||
});
|
||||
|
||||
if (!isDefined(fieldMetadataMorphRelation?.targetObjectMetadata)) {
|
||||
throw new Error(
|
||||
'Should never occur, encountered invalid relation definition',
|
||||
);
|
||||
}
|
||||
|
||||
const cachedRecord = getRecordFromCache({
|
||||
cache,
|
||||
objectMetadataItem: fieldMetadataMorphRelation.targetObjectMetadata,
|
||||
objectMetadataItems,
|
||||
recordId: recordInputFieldIdValue as string,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
});
|
||||
|
||||
optimisticRecord[relationFieldIdName] = recordInputFieldIdValue;
|
||||
|
||||
if (!isDefined(cachedRecord) || Object.keys(cachedRecord).length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
optimisticRecord[relationFieldName] = cachedRecord;
|
||||
}
|
||||
|
||||
optimisticRecord[fieldMetadataItem.name] = cachedRecord;
|
||||
}
|
||||
|
||||
return optimisticRecord;
|
||||
|
||||
Reference in New Issue
Block a user