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:
Charles Bochet
2024-04-01 13:12:37 +02:00
committed by GitHub
parent 4e109c9a38
commit 02673a82af
172 changed files with 2182 additions and 4915 deletions

View File

@ -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',
)!,

View File

@ -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
}
}`);
});
});

View File

@ -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);

View File

@ -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}`,
);
}
};

View File

@ -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,
})}
}
}`;

View File

@ -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')}

View File

@ -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;
}