Feat/put target object identifier on use activities (#4682)
When writing to the normalized cache (record), it's crucial to use _refs for relationships to avoid many problems. Essentially, we only deal with level 0 and generate all fields to be comfortable with their defaults. When writing in queries (which should be very rare, the only cases are prefetch and the case of activities due to the nested query; I've reduced this to a single file for activities usePrepareFindManyActivitiesQuery 🙂), it's important to use queryFields to avoid bugs. I've implemented them on the side of query generation and record generation. When doing an updateOne / createOne, etc., it's necessary to distinguish between optimistic writing (which we actually want to do with _refs) and the server response without refs. This allows for a clean write in the optimistic cache without worrying about nesting (as the first point). To simplify the whole activities part, write to the normalized cache first. Then, base queries on it in an idempotent manner. This way, there's no need to worry about the current page or action. The normalized cache is up-to-date, so I update the queries. Same idea as for optimisticEffects, actually. Finally, I've triggered optimisticEffects rather than the manual update of many queries. --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -40,7 +40,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
|
||||
it('should not return relation if depth is < 1', async () => {
|
||||
const res = mapFieldMetadataToGraphQLQuery({
|
||||
objectMetadataItems: mockObjectMetadataItems,
|
||||
relationFieldDepth: 0,
|
||||
depth: 0,
|
||||
field: personObjectMetadataItem.fields.find(
|
||||
(field) => field.name === 'company',
|
||||
)!,
|
||||
@ -51,7 +51,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
|
||||
it('should return relation if it matches depth', async () => {
|
||||
const res = mapFieldMetadataToGraphQLQuery({
|
||||
objectMetadataItems: mockObjectMetadataItems,
|
||||
relationFieldDepth: 1,
|
||||
depth: 1,
|
||||
field: personObjectMetadataItem.fields.find(
|
||||
(field) => field.name === 'company',
|
||||
)!,
|
||||
@ -88,7 +88,7 @@ idealCustomerProfile
|
||||
it('should return relation with all sub relations if it matches depth', async () => {
|
||||
const res = mapFieldMetadataToGraphQLQuery({
|
||||
objectMetadataItems: mockObjectMetadataItems,
|
||||
relationFieldDepth: 2,
|
||||
depth: 2,
|
||||
field: personObjectMetadataItem.fields.find(
|
||||
(field) => field.name === 'company',
|
||||
)!,
|
||||
@ -239,11 +239,26 @@ idealCustomerProfile
|
||||
}`);
|
||||
});
|
||||
|
||||
it('should return eagerLoaded relations', async () => {
|
||||
it('should return GraphQL fields based on queryFields', async () => {
|
||||
const res = mapFieldMetadataToGraphQLQuery({
|
||||
objectMetadataItems: mockObjectMetadataItems,
|
||||
relationFieldDepth: 2,
|
||||
relationFieldEagerLoad: { accountOwner: true, people: true },
|
||||
depth: 2,
|
||||
queryFields: {
|
||||
accountOwner: true,
|
||||
people: true,
|
||||
xLink: true,
|
||||
linkedinLink: true,
|
||||
domainName: true,
|
||||
annualRecurringRevenue: true,
|
||||
createdAt: true,
|
||||
address: true,
|
||||
updatedAt: true,
|
||||
name: true,
|
||||
accountOwnerId: true,
|
||||
employees: true,
|
||||
id: true,
|
||||
idealCustomerProfile: true,
|
||||
},
|
||||
field: personObjectMetadataItem.fields.find(
|
||||
(field) => field.name === 'company',
|
||||
)!,
|
||||
|
||||
@ -213,11 +213,25 @@ companyId
|
||||
}`);
|
||||
});
|
||||
|
||||
it('should eager load only specified relations', async () => {
|
||||
it('should query only specified queryFields', async () => {
|
||||
const res = mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems: mockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
eagerLoadedRelations: { company: true },
|
||||
queryFields: {
|
||||
company: true,
|
||||
xLink: true,
|
||||
id: true,
|
||||
createdAt: true,
|
||||
city: true,
|
||||
email: true,
|
||||
jobTitle: true,
|
||||
name: true,
|
||||
phone: true,
|
||||
linkedinLink: true,
|
||||
updatedAt: true,
|
||||
avatarUrl: true,
|
||||
companyId: true,
|
||||
},
|
||||
depth: 1,
|
||||
});
|
||||
expect(formatGQLString(res)).toEqual(`{
|
||||
@ -274,6 +288,52 @@ linkedinLink
|
||||
updatedAt
|
||||
avatarUrl
|
||||
companyId
|
||||
}`);
|
||||
});
|
||||
|
||||
it('should load only specified query fields', async () => {
|
||||
const res = mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems: mockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
queryFields: { company: true, id: true, name: true },
|
||||
depth: 1,
|
||||
});
|
||||
expect(formatGQLString(res)).toEqual(`{
|
||||
__typename
|
||||
id
|
||||
company
|
||||
{
|
||||
__typename
|
||||
xLink
|
||||
{
|
||||
label
|
||||
url
|
||||
}
|
||||
linkedinLink
|
||||
{
|
||||
label
|
||||
url
|
||||
}
|
||||
domainName
|
||||
annualRecurringRevenue
|
||||
{
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
createdAt
|
||||
address
|
||||
updatedAt
|
||||
name
|
||||
accountOwnerId
|
||||
employees
|
||||
id
|
||||
idealCustomerProfile
|
||||
}
|
||||
name
|
||||
{
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -34,10 +34,10 @@ describe('shouldFieldBeQueried', () => {
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('should not depends on eagerLoadedRelation', () => {
|
||||
it('should not depends on queryFields', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 0,
|
||||
eagerLoadedRelations: {
|
||||
queryFields: {
|
||||
fieldName: true,
|
||||
},
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Boolean },
|
||||
@ -47,14 +47,14 @@ describe('shouldFieldBeQueried', () => {
|
||||
});
|
||||
|
||||
describe('if field is relation', () => {
|
||||
it('should be queried if eagerLoadedRelation and depth are undefined', () => {
|
||||
it('should be queried if queryFields and depth are undefined', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
});
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('should be queried if eagerLoadedRelation is undefined and depth = 1', () => {
|
||||
it('should be queried if queryFields is undefined and depth = 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 1,
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
@ -62,7 +62,7 @@ describe('shouldFieldBeQueried', () => {
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('should be queried if eagerLoadedRelation is undefined and depth > 1', () => {
|
||||
it('should be queried if queryFields is undefined and depth > 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 2,
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
@ -70,7 +70,7 @@ describe('shouldFieldBeQueried', () => {
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT be queried if eagerLoadedRelation is undefined and depth < 1', () => {
|
||||
it('should NOT be queried if queryFields is undefined and depth < 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 0,
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
@ -78,37 +78,37 @@ describe('shouldFieldBeQueried', () => {
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('should be queried if eagerLoadedRelation is matching and depth > 1', () => {
|
||||
it('should be queried if queryFields is matching and depth > 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 1,
|
||||
eagerLoadedRelations: { fieldName: true },
|
||||
queryFields: { fieldName: true },
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
});
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT be queried if eagerLoadedRelation is matching and depth < 1', () => {
|
||||
it('should NOT be queried if queryFields is matching and depth < 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 0,
|
||||
eagerLoadedRelations: { fieldName: true },
|
||||
queryFields: { fieldName: true },
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
});
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT be queried if eagerLoadedRelation is not matching (falsy) and depth < 1', () => {
|
||||
it('should NOT be queried if queryFields is not matching (falsy) and depth < 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 1,
|
||||
eagerLoadedRelations: { fieldName: false },
|
||||
queryFields: { fieldName: false },
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
});
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT be queried if eagerLoadedRelation is not matching and depth < 1', () => {
|
||||
it('should NOT be queried if queryFields is not matching and depth < 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 0,
|
||||
eagerLoadedRelations: { anotherFieldName: true },
|
||||
queryFields: { anotherFieldName: true },
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
});
|
||||
expect(res).toBe(false);
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { RelationDirections } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationDefinitionType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const getFieldRelationDirections = (
|
||||
field: Pick<FieldMetadataItem, 'type' | 'relationDefinition'> | undefined,
|
||||
): RelationDirections => {
|
||||
if (!field || field.type !== FieldMetadataType.Relation) {
|
||||
throw new Error(`Field is not a relation field.`);
|
||||
}
|
||||
|
||||
switch (field.relationDefinition?.direction) {
|
||||
case RelationDefinitionType.ManyToMany:
|
||||
throw new Error(`Many to many relations are not supported.`);
|
||||
case RelationDefinitionType.OneToMany:
|
||||
return {
|
||||
from: 'FROM_ONE_OBJECT',
|
||||
to: 'TO_MANY_OBJECTS',
|
||||
};
|
||||
case RelationDefinitionType.ManyToOne:
|
||||
return {
|
||||
from: 'FROM_MANY_OBJECTS',
|
||||
to: 'TO_ONE_OBJECT',
|
||||
};
|
||||
case RelationDefinitionType.OneToOne:
|
||||
return {
|
||||
from: 'FROM_ONE_OBJECT',
|
||||
to: 'TO_ONE_OBJECT',
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid relation definition type direction : ${field.relationDefinition?.direction}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -6,19 +6,22 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
|
||||
// TODO: change ObjectMetadataItems mock before refactoring with relationDefinition computed field
|
||||
export const mapFieldMetadataToGraphQLQuery = ({
|
||||
objectMetadataItems,
|
||||
field,
|
||||
relationFieldDepth = 0,
|
||||
relationFieldEagerLoad,
|
||||
depth = 0,
|
||||
queryFields,
|
||||
computeReferences = false,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
field: Pick<
|
||||
FieldMetadataItem,
|
||||
'name' | 'type' | 'toRelationMetadata' | 'fromRelationMetadata'
|
||||
>;
|
||||
relationFieldDepth?: number;
|
||||
relationFieldEagerLoad?: Record<string, any>;
|
||||
depth?: number;
|
||||
queryFields?: Record<string, any>;
|
||||
computeReferences?: boolean;
|
||||
}): any => {
|
||||
const fieldType = field.type;
|
||||
|
||||
@ -43,7 +46,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
} else if (
|
||||
fieldType === 'RELATION' &&
|
||||
field.toRelationMetadata?.relationType === 'ONE_TO_MANY' &&
|
||||
relationFieldDepth > 0
|
||||
depth > 0
|
||||
) {
|
||||
const relationMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
@ -59,13 +62,15 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem: relationMetadataItem,
|
||||
eagerLoadedRelations: relationFieldEagerLoad,
|
||||
depth: relationFieldDepth - 1,
|
||||
depth: depth - 1,
|
||||
queryFields,
|
||||
computeReferences: computeReferences,
|
||||
isRootLevel: false,
|
||||
})}`;
|
||||
} else if (
|
||||
fieldType === 'RELATION' &&
|
||||
field.fromRelationMetadata?.relationType === 'ONE_TO_MANY' &&
|
||||
relationFieldDepth > 0
|
||||
depth > 0
|
||||
) {
|
||||
const relationMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
@ -83,8 +88,10 @@ ${mapObjectMetadataToGraphQLQuery({
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem: relationMetadataItem,
|
||||
eagerLoadedRelations: relationFieldEagerLoad,
|
||||
depth: relationFieldDepth - 1,
|
||||
depth: depth - 1,
|
||||
queryFields,
|
||||
computeReferences,
|
||||
isRootLevel: false,
|
||||
})}
|
||||
}
|
||||
}`;
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
|
||||
import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueried';
|
||||
@ -8,28 +6,47 @@ export const mapObjectMetadataToGraphQLQuery = ({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
depth = 1,
|
||||
eagerLoadedRelations,
|
||||
queryFields,
|
||||
computeReferences = false,
|
||||
isRootLevel = true,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItem: Pick<ObjectMetadataItem, 'nameSingular' | 'fields'>;
|
||||
depth?: number;
|
||||
eagerLoadedRelations?: Record<string, any>;
|
||||
queryFields?: Record<string, any>;
|
||||
computeReferences?: boolean;
|
||||
isRootLevel?: boolean;
|
||||
}): any => {
|
||||
const fieldsThatShouldBeQueried =
|
||||
objectMetadataItem?.fields
|
||||
.filter((field) => field.isActive)
|
||||
.filter((field) =>
|
||||
shouldFieldBeQueried({
|
||||
field,
|
||||
depth,
|
||||
queryFields,
|
||||
}),
|
||||
) ?? [];
|
||||
|
||||
if (!isRootLevel && computeReferences) {
|
||||
return `{
|
||||
__ref
|
||||
}`;
|
||||
}
|
||||
|
||||
return `{
|
||||
__typename
|
||||
${(objectMetadataItem?.fields ?? [])
|
||||
.filter((field) => field.isActive)
|
||||
.filter((field) =>
|
||||
shouldFieldBeQueried({ field, depth, eagerLoadedRelations }),
|
||||
)
|
||||
${fieldsThatShouldBeQueried
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
field,
|
||||
relationFieldDepth: depth,
|
||||
relationFieldEagerLoad: isUndefined(eagerLoadedRelations)
|
||||
? undefined
|
||||
: eagerLoadedRelations[field.name] ?? undefined,
|
||||
depth,
|
||||
queryFields:
|
||||
typeof queryFields?.[field.name] === 'boolean'
|
||||
? undefined
|
||||
: queryFields?.[field.name],
|
||||
computeReferences,
|
||||
}),
|
||||
)
|
||||
.join('\n')}
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
|
||||
export const shouldFieldBeQueried = ({
|
||||
field,
|
||||
depth,
|
||||
eagerLoadedRelations,
|
||||
queryFields,
|
||||
}: {
|
||||
field: Pick<FieldMetadataItem, 'name' | 'type'>;
|
||||
depth?: number;
|
||||
eagerLoadedRelations?: Record<string, boolean>;
|
||||
objectRecord?: ObjectRecord;
|
||||
queryFields?: Record<string, any>;
|
||||
}): any => {
|
||||
if (!isUndefined(depth) && depth < 0) {
|
||||
return false;
|
||||
@ -25,12 +28,7 @@ export const shouldFieldBeQueried = ({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
field.type === FieldMetadataType.Relation &&
|
||||
!isUndefined(eagerLoadedRelations) &&
|
||||
(isUndefined(eagerLoadedRelations[field.name]) ||
|
||||
!eagerLoadedRelations[field.name])
|
||||
) {
|
||||
if (isDefined(queryFields) && !queryFields[field.name]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user