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

@ -1,783 +0,0 @@
import { Company } from '@/companies/types/Company';
import { Favorite } from '@/favorites/types/Favorite';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { Person } from '@/people/types/Person';
export const emptyConnectionMock: ObjectRecordConnection = {
edges: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
totalCount: 0,
__typename: 'ObjectRecordConnection',
};
export const companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock: ObjectRecordConnection<
Partial<Company> &
Pick<Company, 'id'> & {
people: ObjectRecordConnection<
Pick<Person, 'id' | 'name'> & {
favorites: ObjectRecordConnection<
Pick<Favorite, 'id' | 'personId' | 'companyId' | 'position'>
>;
}
>;
}
> = {
pageInfo: {
endCursor: 'WyJmZTI1NmIzOS0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==',
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'WyIwNGIyZTlmNS0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==',
},
edges: [
{
cursor: 'WyIwNGIyZTlmNS0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==',
node: {
id: '04b2e9f5-0713-40a5-8216-82802401d33e',
name: 'Qonto',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor: 'WyIwZDk0MDk5Ny1jMjFlLTRlYzItODczYi1kZTQyNjRkODkwMjUiXQ==',
node: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
people: {
edges: [
{
cursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZGYiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049190df',
name: {
firstName: 'Bertrand',
lastName: 'Voulzy',
},
favorites: {
edges: [
{
cursor:
'WyJjODVhODY3Yy01YThmLTQ4NjEtOGVkMi05NmMzOTAyNDg0MjMiXQ==',
node: {
id: 'c85a867c-5a8f-4861-8ed2-96c390248423',
personId: '240da2ec-2d40-4e49-8df4-9c6a049190df',
companyId: null,
position: 2,
},
},
],
pageInfo: {
endCursor:
'WyJjODVhODY3Yy01YThmLTQ4NjEtOGVkMi05NmMzOTAyNDg0MjMiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyJjODVhODY3Yy01YThmLTQ4NjEtOGVkMi05NmMzOTAyNDg0MjMiXQ==',
},
totalCount: 1,
},
},
},
{
cursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZWYiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049190ef',
name: {
firstName: 'Madison',
lastName: 'Perez',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor:
'WyI1Njk1NTQyMi01ZDU0LTQxYjctYmEzNi1mMGQyMGUxNDE3YWUiXQ==',
node: {
id: '56955422-5d54-41b7-ba36-f0d20e1417ae',
name: {
firstName: 'Avery',
lastName: 'Carter',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor:
'WyI3NTUwMzVkYi02MjNkLTQxZmUtOTJlNy1kZDQ1YjdjNTY4ZTEiXQ==',
node: {
id: '755035db-623d-41fe-92e7-dd45b7c568e1',
name: {
firstName: 'Ethan',
lastName: 'Mitchell',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor:
'WyJhMmU3OGE1Zi0zMzhiLTQ2ZGYtODgxMS1mYTA4YzdkMTlkMzUiXQ==',
node: {
id: 'a2e78a5f-338b-46df-8811-fa08c7d19d35',
name: {
firstName: 'Elizabeth',
lastName: 'Baker',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor:
'WyJjYTFmNWJmMy02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==',
node: {
id: 'ca1f5bf3-64ad-4b0e-bbfd-e9fd795b7016',
name: {
firstName: 'Christopher',
lastName: 'Nelson',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
],
pageInfo: {
endCursor:
'WyJjYTFmNWJmMy02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZGYiXQ==',
},
totalCount: 6,
},
},
},
{
cursor: 'WyIxMTg5OTVmMy01ZDgxLTQ2ZDYtYmY4My1mN2ZkMzNlYTYxMDIiXQ==',
node: {
id: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
name: 'Facebook',
people: {
edges: [
{
cursor:
'WyI5M2M3MmQyZS1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==',
node: {
id: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
name: {
firstName: 'Christopher',
lastName: 'Gonzalez',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor:
'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
node: {
id: 'eeeacacf-eee1-4690-ad2c-8619e5b56a2e',
name: {
firstName: 'Ashley',
lastName: 'Parker',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
],
pageInfo: {
endCursor:
'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyI5M2M3MmQyZS1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==',
},
totalCount: 2,
},
},
},
{
cursor: 'WyIxZDNhMWM2ZS03MDdlLTQ0ZGMtYTFkMi0zMDAzMGJmMWE5NDQiXQ==',
node: {
id: '1d3a1c6e-707e-44dc-a1d2-30030bf1a944',
name: 'Netflix',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor: 'WyI0NjBiNmZiMS1lZDg5LTQxM2EtYjMxYS05NjI5ODZlNjdiYjQiXQ==',
node: {
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
name: 'Microsoft',
people: {
edges: [
{
cursor:
'WyIxZDE1MTg1Mi00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==',
node: {
id: '1d151852-490f-4466-8391-733cfd66a0c8',
name: {
firstName: 'Isabella',
lastName: 'Scott',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor:
'WyI5ODQwNmUyNi04MGYxLTRkZmYtYjU3MC1hNzQ5NDI1MjhkZTMiXQ==',
node: {
id: '98406e26-80f1-4dff-b570-a74942528de3',
name: {
firstName: 'Matthew',
lastName: 'Green',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor:
'WyI5YjMyNGE4OC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==',
node: {
id: '9b324a88-6784-4449-afdf-dc62cb8702f2',
name: {
firstName: 'Nicholas',
lastName: 'Wright',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
],
pageInfo: {
endCursor:
'WyI5YjMyNGE4OC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyIxZDE1MTg1Mi00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==',
},
totalCount: 3,
},
},
},
{
cursor: 'WyI3YTkzZDFlNS0zZjc0LTQ5MmQtYTEwMS0yYTcwZjUwYTE2NDUiXQ==',
node: {
id: '7a93d1e5-3f74-492d-a101-2a70f50a1645',
name: 'Libeo',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor: 'WyI4OWJiODI1Yy0xNzFlLTRiY2MtOWNmNy00MzQ0OGQ2ZmIyNzgiXQ==',
node: {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
name: 'Airbnb',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor: 'WyI5ZDE2MmRlNi1jZmJmLTQxNTYtYTc5MC1lMzk4NTRkY2Q0ZWIiXQ==',
node: {
id: '9d162de6-cfbf-4156-a790-e39854dcd4eb',
name: 'Claap',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor: 'WyJhNjc0ZmE2Yy0xNDU1LTRjNTctYWZhZi1kZDVkYzA4NjM2MWQiXQ==',
node: {
id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d',
name: 'Algolia',
people: {
edges: [
{
cursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049191df',
name: {
firstName: 'Lorie',
lastName: 'Vladim',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
],
pageInfo: {
endCursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==',
},
totalCount: 1,
},
},
},
{
cursor: 'WyJhN2JjNjhkNS1mNzllLTQwZGQtYmQwNi1jMzZlNmFiYjQ2NzgiXQ==',
node: {
id: 'a7bc68d5-f79e-40dd-bd06-c36e6abb4678',
name: 'Samsung',
people: {
edges: [
{
cursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049191de',
name: {
firstName: 'Louis',
lastName: 'Duss',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
],
pageInfo: {
endCursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==',
},
totalCount: 1,
},
},
},
{
cursor: 'WyJhYWZmY2ZiZC1mODZiLTQxOWYtYjc5NC0wMjMxOWFiZTg2MzciXQ==',
node: {
id: 'aaffcfbd-f86b-419f-b794-02319abe8637',
name: 'Hasura',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor: 'WyJmMzNkYzI0Mi01NTE4LTQ1NTMtOTQzMy00MmQ4ZWI4MjgzNGIiXQ==',
node: {
id: 'f33dc242-5518-4553-9433-42d8eb82834b',
name: 'Wework',
people: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
{
cursor: 'WyJmZTI1NmIzOS0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==',
node: {
id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
name: 'Linkedin',
people: {
edges: [
{
cursor:
'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
node: {
id: '0aa00beb-ac73-4797-824e-87a1f5aea9e0',
name: {
firstName: 'Sylvie',
lastName: 'Palmer',
},
favorites: {
edges: [
{
cursor:
'WyIzN2I5NzE0MC0yNmI5LTQ5OGMtODM3Yi00ZjNkZTQ5OWFkODMiXQ==',
node: {
id: '37b97140-26b9-498c-837b-4f3de499ad83',
personId: '0aa00beb-ac73-4797-824e-87a1f5aea9e0',
companyId: null,
position: 1,
},
},
],
pageInfo: {
endCursor:
'WyIzN2I5NzE0MC0yNmI5LTQ5OGMtODM3Yi00ZjNkZTQ5OWFkODMiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyIzN2I5NzE0MC0yNmI5LTQ5OGMtODM3Yi00ZjNkZTQ5OWFkODMiXQ==',
},
totalCount: 1,
},
},
},
{
cursor:
'WyI4NjA4MzE0MS0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==',
node: {
id: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
name: {
firstName: 'Christoph',
lastName: 'Callisto',
},
favorites: {
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
},
totalCount: 0,
},
},
},
],
pageInfo: {
endCursor:
'WyI4NjA4MzE0MS0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
},
totalCount: 2,
},
},
},
],
totalCount: 13,
};
export const peopleWithTheirUniqueCompanies: ObjectRecordConnection<
Pick<Person, 'id'> & { company: Pick<Company, 'id' | 'name'> }
> = {
pageInfo: {
endCursor: 'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
},
totalCount: 15,
edges: [
{
cursor: 'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
node: {
id: '0aa00beb-ac73-4797-824e-87a1f5aea9e0',
company: {
id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
name: 'Linkedin',
},
},
},
{
cursor: 'WyIxZDE1MTg1Mi00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==',
node: {
id: '1d151852-490f-4466-8391-733cfd66a0c8',
company: {
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
name: 'Microsoft',
},
},
},
{
cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZGYiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049190df',
company: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
},
},
},
{
cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZWYiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049190ef',
company: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
},
},
},
{
cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049191de',
company: {
id: 'a7bc68d5-f79e-40dd-bd06-c36e6abb4678',
name: 'Samsung',
},
},
},
{
cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==',
node: {
id: '240da2ec-2d40-4e49-8df4-9c6a049191df',
company: {
id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d',
name: 'Algolia',
},
},
},
{
cursor: 'WyI1Njk1NTQyMi01ZDU0LTQxYjctYmEzNi1mMGQyMGUxNDE3YWUiXQ==',
node: {
id: '56955422-5d54-41b7-ba36-f0d20e1417ae',
company: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
},
},
},
{
cursor: 'WyI3NTUwMzVkYi02MjNkLTQxZmUtOTJlNy1kZDQ1YjdjNTY4ZTEiXQ==',
node: {
id: '755035db-623d-41fe-92e7-dd45b7c568e1',
company: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
},
},
},
{
cursor: 'WyI4NjA4MzE0MS0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==',
node: {
id: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
company: {
id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
name: 'Linkedin',
},
},
},
{
cursor: 'WyI5M2M3MmQyZS1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==',
node: {
id: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
company: {
id: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
name: 'Facebook',
},
},
},
{
cursor: 'WyI5ODQwNmUyNi04MGYxLTRkZmYtYjU3MC1hNzQ5NDI1MjhkZTMiXQ==',
node: {
id: '98406e26-80f1-4dff-b570-a74942528de3',
company: {
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
name: 'Microsoft',
},
},
},
{
cursor: 'WyI5YjMyNGE4OC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==',
node: {
id: '9b324a88-6784-4449-afdf-dc62cb8702f2',
company: {
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
name: 'Microsoft',
},
},
},
{
cursor: 'WyJhMmU3OGE1Zi0zMzhiLTQ2ZGYtODgxMS1mYTA4YzdkMTlkMzUiXQ==',
node: {
id: 'a2e78a5f-338b-46df-8811-fa08c7d19d35',
company: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
},
},
},
{
cursor: 'WyJjYTFmNWJmMy02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==',
node: {
id: 'ca1f5bf3-64ad-4b0e-bbfd-e9fd795b7016',
company: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
},
},
},
{
cursor: 'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
node: {
id: 'eeeacacf-eee1-4690-ad2c-8619e5b56a2e',
company: {
id: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
name: 'Facebook',
},
},
},
],
};

View File

@ -53,7 +53,6 @@ describe('useCreateOneRecord', () => {
await act(async () => {
const res = await result.current.createOneRecord(input);
console.log('res', res);
expect(res).toBeDefined();
expect(res).toHaveProperty('id', personId);
});

View File

@ -84,14 +84,5 @@ describe('useFindManyRecords', () => {
expect(result.current.loading).toBe(true);
expect(result.current.error).toBeUndefined();
expect(result.current.records.length).toBe(0);
// FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
// await waitFor(() => {
// expect(result.current.loading).toBe(false);
// expect(result.current.records).toBeDefined();
// console.log({ res: result.current.records });
// expect(result.current.records.length > 0).toBe(true);
// });
});
});

View File

@ -3,7 +3,7 @@ import { renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>{children}</RecoilRoot>

View File

@ -1,190 +0,0 @@
import { isNonEmptyArray } from '@sniptt/guards';
import { renderHook } from '@testing-library/react';
import { Company } from '@/companies/types/Company';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import {
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock,
emptyConnectionMock,
peopleWithTheirUniqueCompanies,
} from '@/object-record/hooks/__mocks__/useMapConnectionToRecords';
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
import { Person } from '@/people/types/Person';
import { getJestHookWrapper } from '~/testing/jest/getJestHookWrapper';
import { isDefined } from '~/utils/isDefined';
const Wrapper = getJestHookWrapper({
apolloMocks: [],
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(objectMetadataItemsState, getObjectMetadataItemsMock());
},
});
describe('useMapConnectionToRecords', () => {
it('Empty edges - should return an empty array if no edge', async () => {
const { result } = renderHook(
() => {
const mapConnectionToRecords = useMapConnectionToRecords();
const records = mapConnectionToRecords({
objectNameSingular: CoreObjectNameSingular.Company,
objectRecordConnection: emptyConnectionMock,
depth: 5,
});
return records;
},
{
wrapper: Wrapper,
},
);
expect(Array.isArray(result.current)).toBe(true);
});
it('No relation fields - should return an array of company records', async () => {
const { result } = renderHook(
() => {
const mapConnectionToRecords = useMapConnectionToRecords();
const records = mapConnectionToRecords({
objectNameSingular: CoreObjectNameSingular.Company,
objectRecordConnection:
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock,
depth: 5,
});
return records;
},
{
wrapper: Wrapper,
},
);
expect(Array.isArray(result.current)).toBe(true);
});
it('n+1 relation fields - should return an array of company records with their people records', async () => {
const { result } = renderHook(
() => {
const mapConnectionToRecords = useMapConnectionToRecords();
const records = mapConnectionToRecords({
objectNameSingular: CoreObjectNameSingular.Company,
objectRecordConnection:
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock,
depth: 5,
});
return records;
},
{
wrapper: Wrapper,
},
);
const secondCompanyMock =
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock
.edges[1];
const secondCompanyPeopleMock = secondCompanyMock.node.people.edges.map(
(edge) => edge.node,
);
const companiesResult = result.current;
const secondCompanyResult = result.current[1];
const secondCompanyPeopleResult = secondCompanyResult.people;
expect(isNonEmptyArray(companiesResult)).toBe(true);
expect(secondCompanyResult.id).toBe(secondCompanyMock.node.id);
expect(isNonEmptyArray(secondCompanyPeopleResult)).toBe(true);
expect(secondCompanyPeopleResult[0].id).toEqual(
secondCompanyPeopleMock[0].id,
);
});
it('n+2 relation fields - should return an array of company records with their people records with their favorites records', async () => {
const { result } = renderHook(
() => {
const mapConnectionToRecords = useMapConnectionToRecords();
const records = mapConnectionToRecords({
objectNameSingular: CoreObjectNameSingular.Company,
objectRecordConnection:
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock,
depth: 5,
});
return records;
},
{
wrapper: Wrapper,
},
);
const secondCompanyMock =
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock
.edges[1];
const secondCompanyPeopleMock = secondCompanyMock.node.people;
const secondCompanyFirstPersonMock = secondCompanyPeopleMock.edges[0].node;
const secondCompanyFirstPersonFavoritesMock =
secondCompanyFirstPersonMock.favorites;
const companiesResult = result.current;
const secondCompanyResult = companiesResult[1];
const secondCompanyPeopleResult = secondCompanyResult.people;
const secondCompanyFirstPersonResult = secondCompanyPeopleResult[0];
const secondCompanyFirstPersonFavoritesResult =
secondCompanyFirstPersonResult.favorites;
expect(isNonEmptyArray(companiesResult)).toBe(true);
expect(secondCompanyResult.id).toBe(secondCompanyMock.node.id);
expect(isNonEmptyArray(secondCompanyPeopleResult)).toBe(true);
expect(secondCompanyFirstPersonResult.id).toEqual(
secondCompanyFirstPersonMock.id,
);
expect(isNonEmptyArray(secondCompanyFirstPersonFavoritesResult)).toBe(true);
expect(secondCompanyFirstPersonFavoritesResult[0].id).toEqual(
secondCompanyFirstPersonFavoritesMock.edges[0].node.id,
);
});
it("n+1 relation field TO_ONE_OBJECT - should return an array of people records with their company, mapConnectionToRecords shouldn't try to parse TO_ONE_OBJECT", async () => {
const { result } = renderHook(
() => {
const mapConnectionToRecords = useMapConnectionToRecords();
const records = mapConnectionToRecords({
objectNameSingular: CoreObjectNameSingular.Person,
objectRecordConnection: peopleWithTheirUniqueCompanies,
depth: 5,
});
return records as (Person & { company: Company })[];
},
{
wrapper: Wrapper,
},
);
const firstPersonMock = peopleWithTheirUniqueCompanies.edges[0].node;
const firstPersonsCompanyMock = firstPersonMock.company;
const peopleResult = result.current;
const firstPersonResult = result.current[0];
const firstPersonsCompanyresult = firstPersonResult.company;
expect(isNonEmptyArray(peopleResult)).toBe(true);
expect(firstPersonResult.id).toBe(firstPersonMock.id);
expect(isDefined(firstPersonsCompanyresult)).toBe(true);
expect(firstPersonsCompanyresult.id).toEqual(firstPersonsCompanyMock.id);
});
});

View File

@ -1,52 +0,0 @@
import { ReactNode } from 'react';
import { useApolloClient } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider addTypename={false}>
<RecoilRoot>{children}</RecoilRoot>
</MockedProvider>
);
const recordId = '91408718-a29f-4678-b573-c791e8664c2a';
describe('useModifyRecordFromCache', () => {
it('should work as expected', async () => {
const { result } = renderHook(
() => {
const apolloClient = useApolloClient();
const mockObjectMetadataItems = getObjectMetadataItemsMock();
const personMetadataItem = mockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
return {
modifyRecordFromCache: useModifyRecordFromCache({
objectMetadataItem: personMetadataItem,
}),
cache: apolloClient.cache,
};
},
{
wrapper: Wrapper,
},
);
const spy = jest.spyOn(result.current.cache, 'modify');
act(() => {
result.current.modifyRecordFromCache(recordId, {});
});
expect(spy).toHaveBeenCalledWith({
id: `Person:${recordId}`,
fields: {},
});
});
});

View File

@ -2,57 +2,87 @@ import { useApolloClient } from '@apollo/client';
import { v4 } from 'uuid';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
import {
getCreateManyRecordsMutationResponseField,
useGenerateCreateManyRecordMutation,
} from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { isDefined } from '~/utils/isDefined';
type CreateManyRecordsOptions = {
skipOptimisticEffect?: boolean;
type useCreateManyRecordsProps = {
objectNameSingular: string;
queryFields?: Record<string, any>;
depth?: number;
skipPostOptmisticEffect?: boolean;
};
export const useCreateManyRecords = <
CreatedObjectRecord extends ObjectRecord = ObjectRecord,
>({
objectNameSingular,
}: ObjectMetadataItemIdentifier) => {
queryFields,
depth = 1,
skipPostOptmisticEffect = false,
}: useCreateManyRecordsProps) => {
const apolloClient = useApolloClient();
const { objectMetadataItem, createManyRecordsMutation } =
useObjectMetadataItem({
objectNameSingular,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { generateObjectRecordOptimisticResponse } =
useGenerateObjectRecordOptimisticResponse({
objectMetadataItem,
});
const createManyRecordsMutation = useGenerateCreateManyRecordMutation({
objectMetadataItem,
queryFields,
depth,
});
const createOneRecordInCache = useCreateOneRecordInCache<CachedObjectRecord>({
objectMetadataItem,
});
const { objectMetadataItems } = useObjectMetadataItems();
const createManyRecords = async (
data: Partial<CreatedObjectRecord>[],
options?: CreateManyRecordsOptions,
recordsToCreate: Partial<CreatedObjectRecord>[],
) => {
const sanitizedCreateManyRecordsInput = data.map((input) => {
const idForCreation = input.id ?? v4();
const sanitizedCreateManyRecordsInput = recordsToCreate.map(
(recordToCreate) => {
const idForCreation = recordToCreate?.id ?? v4();
const sanitizedRecordInput = sanitizeRecordInput({
objectMetadataItem,
recordInput: { ...input, id: idForCreation },
});
return sanitizedRecordInput;
});
const optimisticallyCreatedRecords = sanitizedCreateManyRecordsInput.map(
(record) =>
generateObjectRecordOptimisticResponse<CreatedObjectRecord>(record),
return {
...sanitizeRecordInput({
objectMetadataItem,
recordInput: recordToCreate,
}),
id: idForCreation,
};
},
);
const recordsCreatedInCache = [];
for (const recordToCreate of sanitizedCreateManyRecordsInput) {
const recordCreatedInCache = createOneRecordInCache(recordToCreate);
if (isDefined(recordCreatedInCache)) {
recordsCreatedInCache.push(recordCreatedInCache);
}
}
if (recordsCreatedInCache.length > 0) {
triggerCreateRecordsOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
recordsToCreate: recordsCreatedInCache,
objectMetadataItems,
});
}
const mutationResponseField = getCreateManyRecordsMutationResponseField(
objectMetadataItem.namePlural,
);
@ -62,25 +92,18 @@ export const useCreateManyRecords = <
variables: {
data: sanitizedCreateManyRecordsInput,
},
optimisticResponse: options?.skipOptimisticEffect
? undefined
: {
[mutationResponseField]: optimisticallyCreatedRecords,
},
update: options?.skipOptimisticEffect
? undefined
: (cache, { data }) => {
const records = data?.[mutationResponseField];
update: (cache, { data }) => {
const records = data?.[mutationResponseField];
if (!records?.length) return;
if (!records?.length || skipPostOptmisticEffect) return;
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToCreate: records,
objectMetadataItems,
});
},
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToCreate: records,
objectMetadataItems,
});
},
});
return createdObjects.data?.[mutationResponseField] ?? [];

View File

@ -1,49 +0,0 @@
import { v4 } from 'uuid';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache';
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from '~/utils/isDefined';
export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
objectNameSingular,
}: ObjectMetadataItemIdentifier) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { generateObjectRecordOptimisticResponse } =
useGenerateObjectRecordOptimisticResponse({
objectMetadataItem,
});
const addRecordInCache = useAddRecordInCache({
objectMetadataItem,
});
const createManyRecordsInCache = (data: Partial<T>[]) => {
const recordsWithId = data.map((record) => ({
...record,
id: (record.id as string) ?? v4(),
}));
const createdRecordsInCache = [] as T[];
for (const record of recordsWithId) {
const generatedCachedObjectRecord =
generateObjectRecordOptimisticResponse<T>(record);
if (isDefined(generatedCachedObjectRecord)) {
addRecordInCache(generatedCachedObjectRecord);
createdRecordsInCache.push(generatedCachedObjectRecord);
}
}
return createdRecordsInCache;
};
return { createManyRecordsInCache };
};

View File

@ -2,55 +2,73 @@ import { useApolloClient } from '@apollo/client';
import { v4 } from 'uuid';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
import {
getCreateOneRecordMutationResponseField,
useGenerateCreateOneRecordMutation,
} from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { isDefined } from '~/utils/isDefined';
type useCreateOneRecordProps = {
objectNameSingular: string;
};
type CreateOneRecordOptions = {
skipOptimisticEffect?: boolean;
queryFields?: Record<string, any>;
depth?: number;
skipPostOptmisticEffect?: boolean;
};
export const useCreateOneRecord = <
CreatedObjectRecord extends ObjectRecord = ObjectRecord,
>({
objectNameSingular,
queryFields,
depth = 1,
skipPostOptmisticEffect = false,
}: useCreateOneRecordProps) => {
const apolloClient = useApolloClient();
const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem(
{ objectNameSingular },
);
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
const { generateObjectRecordOptimisticResponse } =
useGenerateObjectRecordOptimisticResponse({
objectMetadataItem,
});
const createOneRecordMutation = useGenerateCreateOneRecordMutation({
objectMetadataItem,
queryFields,
depth,
});
const createOneRecordInCache = useCreateOneRecordInCache<CachedObjectRecord>({
objectMetadataItem,
});
const { objectMetadataItems } = useObjectMetadataItems();
const createOneRecord = async (
input: Partial<CreatedObjectRecord>,
options?: CreateOneRecordOptions,
) => {
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
const idForCreation = input.id ?? v4();
const sanitizedCreateOneRecordInput = sanitizeRecordInput({
objectMetadataItem,
recordInput: { ...input, id: idForCreation },
const sanitizedInput = {
...sanitizeRecordInput({
objectMetadataItem,
recordInput: input,
}),
id: idForCreation,
};
const recordCreatedInCache = createOneRecordInCache({
...input,
id: idForCreation,
});
const optimisticallyCreatedRecord =
generateObjectRecordOptimisticResponse<CreatedObjectRecord>({
...input,
...sanitizedCreateOneRecordInput,
if (isDefined(recordCreatedInCache)) {
triggerCreateRecordsOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
recordsToCreate: [recordCreatedInCache],
objectMetadataItems,
});
}
const mutationResponseField =
getCreateOneRecordMutationResponseField(objectNameSingular);
@ -58,27 +76,20 @@ export const useCreateOneRecord = <
const createdObject = await apolloClient.mutate({
mutation: createOneRecordMutation,
variables: {
input: sanitizedCreateOneRecordInput,
input: sanitizedInput,
},
optimisticResponse: options?.skipOptimisticEffect
? undefined
: {
[mutationResponseField]: optimisticallyCreatedRecord,
},
update: options?.skipOptimisticEffect
? undefined
: (cache, { data }) => {
const record = data?.[mutationResponseField];
update: (cache, { data }) => {
const record = data?.[mutationResponseField];
if (!record) return;
if (!record || skipPostOptmisticEffect) return;
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToCreate: [record],
objectMetadataItems,
});
},
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToCreate: [record],
objectMetadataItems,
});
},
});
return createdObject.data?.[mutationResponseField] ?? null;

View File

@ -1,38 +0,0 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache';
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
type useCreateOneRecordInCacheProps = {
objectNameSingular: string;
};
export const useCreateOneRecordInCache = <T>({
objectNameSingular,
}: useCreateOneRecordInCacheProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { generateObjectRecordOptimisticResponse } =
useGenerateObjectRecordOptimisticResponse({
objectMetadataItem,
});
const addRecordInCache = useAddRecordInCache({
objectMetadataItem,
});
const createOneRecordInCache = (input: ObjectRecord) => {
const generatedCachedObjectRecord =
generateObjectRecordOptimisticResponse(input);
addRecordInCache(generatedCachedObjectRecord);
return generatedCachedObjectRecord as T;
};
return {
createOneRecordInCache,
};
};

View File

@ -3,8 +3,8 @@ import { useQuery } from '@apollo/client';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/hooks/useGenerateFindDuplicateRecordsQuery';
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@ -60,16 +60,14 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
const objectRecordConnection = data?.[queryResponseField];
const mapConnectionToRecords = useMapConnectionToRecords();
const records = useMemo(
() =>
mapConnectionToRecords({
objectRecordConnection,
objectNameSingular,
depth: 5,
}) as T[],
[mapConnectionToRecords, objectRecordConnection, objectNameSingular],
objectRecordConnection
? (getRecordsFromRecordConnection({
recordConnection: objectRecordConnection,
}) as T[])
: [],
[objectRecordConnection],
);
return {

View File

@ -7,7 +7,7 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
@ -22,7 +22,6 @@ import { cursorFamilyState } from '../states/cursorFamilyState';
import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState';
import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState';
import { ObjectRecordQueryResult } from '../types/ObjectRecordQueryResult';
import { mapPaginatedRecordsToRecords } from '../utils/mapPaginatedRecordsToRecords';
export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
objectNameSingular,
@ -31,17 +30,20 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
limit,
onCompleted,
skip,
useRecordsWithoutConnection = false,
depth,
depth = 1,
queryFields,
}: ObjectMetadataItemIdentifier &
ObjectRecordQueryVariables & {
onCompleted?: (
data: ObjectRecordConnection<T>,
pageInfo: ObjectRecordConnection<T>['pageInfo'],
records: T[],
options?: {
pageInfo?: ObjectRecordConnection['pageInfo'];
totalCount?: number;
},
) => void;
skip?: boolean;
useRecordsWithoutConnection?: boolean;
depth?: number;
queryFields?: Record<string, any>;
}) => {
const findManyQueryStateIdentifier =
objectNameSingular +
@ -66,6 +68,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
objectNameSingular,
},
depth,
queryFields,
);
const { enqueueSnackBar } = useSnackBar();
@ -81,9 +84,20 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
orderBy,
},
onCompleted: (data) => {
if (!isDefined(data)) {
onCompleted?.([]);
}
const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo;
onCompleted?.(data[objectMetadataItem.namePlural], pageInfo);
const records = getRecordsFromRecordConnection({
recordConnection: data?.[objectMetadataItem.namePlural],
}) as T[];
onCompleted?.(records, {
pageInfo,
totalCount: data?.[objectMetadataItem.namePlural]?.totalCount,
});
if (isDefined(data?.[objectMetadataItem.namePlural])) {
setLastCursor(pageInfo.endCursor ?? '');
@ -132,24 +146,24 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
const pageInfo =
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo;
if (isDefined(data?.[objectMetadataItem.namePlural])) {
setLastCursor(pageInfo.endCursor ?? '');
setHasNextPage(pageInfo.hasNextPage ?? false);
}
onCompleted?.(
{
__typename: `${capitalize(
objectMetadataItem.nameSingular,
)}Connection`,
const records = getRecordsFromRecordConnection({
recordConnection: {
edges: newEdges,
pageInfo:
fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo,
totalCount:
fetchMoreResult?.[objectMetadataItem.namePlural].totalCount,
pageInfo,
},
}) as T[];
onCompleted?.(records, {
pageInfo,
);
totalCount:
fetchMoreResult?.[objectMetadataItem.namePlural]?.totalCount,
});
return Object.assign({}, prev, {
[objectMetadataItem.namePlural]: {
@ -196,40 +210,23 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
enqueueSnackBar,
]);
// TODO: remove this and use only mapConnectionToRecords when we've finished the refactor
const totalCount = data?.[objectMetadataItem.namePlural].totalCount ?? 0;
const records = useMemo(
() =>
mapPaginatedRecordsToRecords({
pagedRecords: data,
objectNamePlural: objectMetadataItem.namePlural,
}) as T[],
[data, objectMetadataItem],
);
data?.[objectMetadataItem.namePlural]
? getRecordsFromRecordConnection({
recordConnection: data?.[objectMetadataItem.namePlural],
})
: ([] as T[]),
const mapConnectionToRecords = useMapConnectionToRecords();
const recordsWithoutConnection = useMemo(
() =>
useRecordsWithoutConnection
? (mapConnectionToRecords({
objectRecordConnection: data?.[objectMetadataItem.namePlural],
objectNameSingular,
depth: 5,
}) as T[])
: [],
[
data,
objectNameSingular,
objectMetadataItem.namePlural,
mapConnectionToRecords,
useRecordsWithoutConnection,
],
[data, objectMetadataItem.namePlural],
);
return {
objectMetadataItem,
records: useRecordsWithoutConnection ? recordsWithoutConnection : records,
totalCount: data?.[objectMetadataItem.namePlural].totalCount || 0,
records,
totalCount,
loading,
error,
fetchMoreRecords,

View File

@ -1,8 +1,11 @@
import { useMemo } from 'react';
import { useQuery } from '@apollo/client';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from '~/utils/isDefined';
// TODO: fix connection in relation => automatically change to an array
export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
@ -28,11 +31,29 @@ export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
>(findOneRecordQuery, {
skip: !objectMetadataItem || !objectRecordId || skip,
variables: { objectRecordId },
onCompleted: (data) => onCompleted?.(data[objectNameSingular]),
onCompleted: (data) => {
const recordWithoutConnection = getRecordFromRecordNode({
recordNode: { ...data[objectNameSingular] },
});
if (isDefined(recordWithoutConnection)) {
onCompleted?.(recordWithoutConnection);
}
},
});
const recordWithoutConnection = useMemo(
() =>
data?.[objectNameSingular]
? getRecordFromRecordNode({
recordNode: data?.[objectNameSingular],
})
: undefined,
[data, objectNameSingular],
);
return {
record: data?.[objectNameSingular] || undefined,
record: recordWithoutConnection,
loading,
error,
};

View File

@ -14,8 +14,12 @@ export const getCreateManyRecordsMutationResponseField = (
export const useGenerateCreateManyRecordMutation = ({
objectMetadataItem,
queryFields,
depth = 1,
}: {
objectMetadataItem: ObjectMetadataItem;
queryFields?: Record<string, any>;
depth?: number;
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
@ -34,6 +38,8 @@ export const useGenerateCreateManyRecordMutation = ({
${mutationResponseField}(data: $data) ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems,
objectMetadataItem,
queryFields,
depth,
})}
}`;
};

View File

@ -14,8 +14,12 @@ export const getCreateOneRecordMutationResponseField = (
export const useGenerateCreateOneRecordMutation = ({
objectMetadataItem,
queryFields,
depth = 1,
}: {
objectMetadataItem: ObjectMetadataItem;
queryFields?: Record<string, any>;
depth?: number;
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
@ -34,6 +38,8 @@ export const useGenerateCreateOneRecordMutation = ({
${mutationResponseField}(data: $input) ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems,
objectMetadataItem,
queryFields,
depth,
})}
}
`;

View File

@ -1,92 +0,0 @@
import { gql } from '@apollo/client';
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
import { capitalize } from '~/utils/string/capitalize';
export const useGenerateFindManyRecordsForMultipleMetadataItemsQuery = ({
targetObjectMetadataItems,
depth,
}: {
targetObjectMetadataItems: ObjectMetadataItem[];
depth?: number;
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const capitalizedObjectNameSingulars = targetObjectMetadataItems.map(
({ nameSingular }) => capitalize(nameSingular),
);
if (!isNonEmptyArray(capitalizedObjectNameSingulars)) {
return null;
}
const filterPerMetadataItemArray = capitalizedObjectNameSingulars
.map(
(capitalizedObjectNameSingular) =>
`$filter${capitalizedObjectNameSingular}: ${capitalizedObjectNameSingular}FilterInput`,
)
.join(', ');
const orderByPerMetadataItemArray = capitalizedObjectNameSingulars
.map(
(capitalizedObjectNameSingular) =>
`$orderBy${capitalizedObjectNameSingular}: ${capitalizedObjectNameSingular}OrderByInput`,
)
.join(', ');
const lastCursorPerMetadataItemArray = capitalizedObjectNameSingulars
.map(
(capitalizedObjectNameSingular) =>
`$lastCursor${capitalizedObjectNameSingular}: String`,
)
.join(', ');
const limitPerMetadataItemArray = capitalizedObjectNameSingulars
.map(
(capitalizedObjectNameSingular) =>
`$limit${capitalizedObjectNameSingular}: Float`,
)
.join(', ');
return gql`
query FindManyRecordsMultipleMetadataItems(
${filterPerMetadataItemArray},
${orderByPerMetadataItemArray},
${lastCursorPerMetadataItemArray},
${limitPerMetadataItemArray}
) {
${targetObjectMetadataItems
.map(
(objectMetadataItem) =>
`${objectMetadataItem.namePlural}(filter: $filter${capitalize(
objectMetadataItem.nameSingular,
)}, orderBy: $orderBy${capitalize(
objectMetadataItem.nameSingular,
)}, first: $limit${capitalize(
objectMetadataItem.nameSingular,
)}, after: $lastCursor${capitalize(
objectMetadataItem.nameSingular,
)}){
edges {
node ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems: objectMetadataItems,
objectMetadataItem,
depth,
})}
cursor
}
pageInfo {
hasNextPage
startCursor
endCursor
}
totalCount
}`,
)
.join('\n')}
}
`;
};

View File

@ -12,14 +12,16 @@ export const useGenerateFindManyRecordsQuery = () => {
return ({
objectMetadataItem,
depth,
eagerLoadedRelations,
queryFields,
computeReferences = false,
}: {
objectMetadataItem: Pick<
ObjectMetadataItem,
'fields' | 'nameSingular' | 'namePlural'
>;
depth?: number;
eagerLoadedRelations?: Record<string, any>;
queryFields?: Record<string, any>;
computeReferences?: boolean;
}) => gql`
query FindMany${capitalize(
objectMetadataItem.namePlural,
@ -36,7 +38,8 @@ export const useGenerateFindManyRecordsQuery = () => {
objectMetadataItems,
objectMetadataItem,
depth,
eagerLoadedRelations,
queryFields,
computeReferences,
})}
cursor
}

View File

@ -14,8 +14,12 @@ export const getUpdateOneRecordMutationResponseField = (
export const useGenerateUpdateOneRecordMutation = ({
objectMetadataItem,
depth = 1,
computeReferences = false,
}: {
objectMetadataItem: ObjectMetadataItem;
depth?: number;
computeReferences?: boolean;
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
@ -35,6 +39,8 @@ export const useGenerateUpdateOneRecordMutation = ({
{
objectMetadataItems,
objectMetadataItem,
depth,
computeReferences,
},
)}
}

View File

@ -1,113 +0,0 @@
import { useCallback } from 'react';
import { isNonEmptyArray } from '@sniptt/guards';
import { produce } from 'immer';
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
export const useMapConnectionToRecords = () => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const mapConnectionToRecords = useCallback(
<T extends ObjectRecord>({
objectRecordConnection,
objectNameSingular,
objectNamePlural,
depth,
}: {
objectRecordConnection: ObjectRecordConnection<T> | undefined | null;
objectNameSingular?: string;
objectNamePlural?: string;
depth: number;
}): ObjectRecord[] => {
if (
!isDefined(objectRecordConnection) ||
!isNonEmptyArray(objectMetadataItems)
) {
return [];
}
const currentLevelObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular ||
objectMetadataItem.namePlural === objectNamePlural,
);
if (!currentLevelObjectMetadataItem) {
throw new Error(
`Could not find object metadata item for object name singular "${objectNameSingular}" in mapConnectionToRecords`,
);
}
const relationFields = currentLevelObjectMetadataItem.fields.filter(
(field) => field.type === FieldMetadataType.Relation,
);
const objectRecords = [
...(objectRecordConnection.edges?.map((edge) => edge.node) ?? []),
];
return produce(objectRecords, (objectRecordsDraft) => {
for (const objectRecordDraft of objectRecordsDraft) {
for (const relationField of relationFields) {
const relationType = parseFieldRelationType(relationField);
if (
relationType === 'TO_ONE_OBJECT' ||
relationType === 'FROM_ONE_OBJECT'
) {
continue;
}
const relatedObjectMetadataSingularName =
relationField.toRelationMetadata?.fromObjectMetadata
.nameSingular ??
relationField.fromRelationMetadata?.toObjectMetadata
.nameSingular ??
null;
const relationFieldMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular ===
relatedObjectMetadataSingularName,
);
if (
!relationFieldMetadataItem ||
!isDefined(relatedObjectMetadataSingularName)
) {
throw new Error(
`Could not find relation object metadata item for object name plural ${relationField.name} in mapConnectionToRecords`,
);
}
const relationConnection = objectRecordDraft?.[
relationField.name
] as ObjectRecordConnection | undefined | null;
if (!isDefined(relationConnection)) {
continue;
}
const relationConnectionMappedToRecords = mapConnectionToRecords({
objectRecordConnection: relationConnection,
objectNameSingular: relatedObjectMetadataSingularName,
depth: depth - 1,
});
(objectRecordDraft as any)[relationField.name] =
relationConnectionMappedToRecords;
}
}
}) as ObjectRecord[];
},
[objectMetadataItems],
);
return mapConnectionToRecords;
};

View File

@ -3,29 +3,29 @@ import { useApolloClient } from '@apollo/client';
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
type useUpdateOneRecordProps = {
objectNameSingular: string;
queryFields?: Record<string, any>;
depth?: number;
};
export const useUpdateOneRecord = <
UpdatedObjectRecord extends ObjectRecord = ObjectRecord,
>({
objectNameSingular,
queryFields,
depth = 1,
}: useUpdateOneRecordProps) => {
const apolloClient = useApolloClient();
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
useObjectMetadataItem({ objectNameSingular }, 1);
const { generateObjectRecordOptimisticResponse } =
useGenerateObjectRecordOptimisticResponse({
objectMetadataItem,
});
useObjectMetadataItem({ objectNameSingular }, depth, queryFields);
const { objectMetadataItems } = useObjectMetadataItems();
@ -36,17 +36,57 @@ export const useUpdateOneRecord = <
idToUpdate: string;
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
}) => {
const sanitizedInput = {
...sanitizeRecordInput({
objectMetadataItem,
recordInput: updateOneRecordInput,
}),
};
const cachedRecord = getRecordFromCache<UpdatedObjectRecord>(idToUpdate);
const sanitizedUpdateOneRecordInput = sanitizeRecordInput({
const cachedRecordWithConnection = getRecordNodeFromRecord<ObjectRecord>({
record: cachedRecord,
objectMetadataItem,
recordInput: updateOneRecordInput,
objectMetadataItems,
depth,
queryFields,
computeReferences: true,
});
const optimisticallyUpdatedRecord = generateObjectRecordOptimisticResponse({
...(cachedRecord ?? {}),
...sanitizedUpdateOneRecordInput,
id: idToUpdate,
const optimisticRecord = {
...cachedRecord,
...sanitizedInput,
...{ id: idToUpdate },
};
const optimisticRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({
record: optimisticRecord,
objectMetadataItem,
objectMetadataItems,
depth,
queryFields,
computeReferences: true,
});
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
return null;
}
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: optimisticRecord,
});
triggerUpdateRecordOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
currentRecord: cachedRecordWithConnection,
updatedRecord: optimisticRecordWithConnection,
objectMetadataItems,
});
const mutationResponseField =
@ -56,10 +96,7 @@ export const useUpdateOneRecord = <
mutation: updateOneRecordMutation,
variables: {
idToUpdate,
input: sanitizedUpdateOneRecordInput,
},
optimisticResponse: {
[mutationResponseField]: optimisticallyUpdatedRecord,
input: sanitizedInput,
},
update: (cache, { data }) => {
const record = data?.[mutationResponseField];

View File

@ -1,24 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useUpsertRecordFieldFromState = () =>
useRecoilCallback(
({ set }) =>
<T extends { id: string }, F extends keyof T>({
record,
fieldName,
}: {
record: T;
fieldName: F extends string ? F : never;
}) =>
set(
recordStoreFamilySelector({ recordId: record.id, fieldName }),
(previousField) =>
isDeeplyEqual(previousField, record[fieldName])
? previousField
: record[fieldName],
),
[],
);