Fixed Apollo cache bug (#16523)
Fixes https://github.com/twentyhq/twenty/issues/16520 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+5
-2
@@ -1,6 +1,9 @@
|
||||
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
|
||||
export type RecordGqlRefConnection = Omit<RecordGqlConnection, 'edges'> & {
|
||||
export type RecordGqlRefConnection = Omit<
|
||||
RecordGqlConnectionEdgesRequired,
|
||||
'edges'
|
||||
> & {
|
||||
edges: RecordGqlRefEdge[];
|
||||
};
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getRecordFromRecordNode should convert a simple record node 1`] = `
|
||||
{
|
||||
"__typename": "Person",
|
||||
"email": "john@example.com",
|
||||
"id": "123",
|
||||
"name": "John Doe",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getRecordFromRecordNode should handle array values 1`] = `
|
||||
{
|
||||
"__typename": "Person",
|
||||
"id": "123",
|
||||
"scores": [
|
||||
100,
|
||||
200,
|
||||
300,
|
||||
],
|
||||
"tags": [
|
||||
"developer",
|
||||
"designer",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getRecordFromRecordNode should handle connection fields with edges 1`] = `
|
||||
{
|
||||
"__typename": "Company",
|
||||
"id": "123",
|
||||
"name": "Acme Inc",
|
||||
"people": [
|
||||
{
|
||||
"__typename": "Person",
|
||||
"id": "456",
|
||||
"name": "John Doe",
|
||||
},
|
||||
{
|
||||
"__typename": "Person",
|
||||
"id": "789",
|
||||
"name": "Jane Smith",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getRecordFromRecordNode should handle deeply nested structures 1`] = `
|
||||
{
|
||||
"__typename": "Workspace",
|
||||
"id": "123",
|
||||
"settings": {
|
||||
"__typename": "WorkspaceSettings",
|
||||
"display": {
|
||||
"__typename": "DisplaySettings",
|
||||
"layout": "compact",
|
||||
"theme": "dark",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getRecordFromRecordNode should handle mixed nested objects and connections 1`] = `
|
||||
{
|
||||
"__typename": "Company",
|
||||
"address": {
|
||||
"__typename": "Address",
|
||||
"city": "New York",
|
||||
"country": "USA",
|
||||
},
|
||||
"employees": [
|
||||
{
|
||||
"__typename": "Person",
|
||||
"id": "456",
|
||||
"name": {
|
||||
"__typename": "FullName",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
},
|
||||
},
|
||||
],
|
||||
"id": "123",
|
||||
"name": "Acme Inc",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getRecordFromRecordNode should handle nested object fields 1`] = `
|
||||
{
|
||||
"__typename": "Person",
|
||||
"id": "123",
|
||||
"name": {
|
||||
"__typename": "FullName",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getRecordFromRecordNode should handle null and undefined values 1`] = `
|
||||
{
|
||||
"__typename": "Person",
|
||||
"email": null,
|
||||
"id": "123",
|
||||
"name": "John Doe",
|
||||
"phone": undefined,
|
||||
}
|
||||
`;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { getRecordFromRecordNode } from '../getRecordFromRecordNode';
|
||||
|
||||
describe('getRecordFromRecordNode', () => {
|
||||
it('should convert a simple record node', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Person',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle nested object fields', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Person',
|
||||
name: {
|
||||
__typename: 'FullName',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle connection fields with edges', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Company',
|
||||
name: 'Acme Inc',
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '456',
|
||||
__typename: 'Person',
|
||||
name: 'John Doe',
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '789',
|
||||
__typename: 'Person',
|
||||
name: 'Jane Smith',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle null and undefined values', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Person',
|
||||
name: 'John Doe',
|
||||
email: null,
|
||||
phone: undefined,
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle array values', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Person',
|
||||
tags: ['developer', 'designer'],
|
||||
scores: [100, 200, 300],
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle deeply nested structures', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Workspace',
|
||||
settings: {
|
||||
__typename: 'WorkspaceSettings',
|
||||
display: {
|
||||
__typename: 'DisplaySettings',
|
||||
theme: 'dark',
|
||||
layout: 'compact',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle mixed nested objects and connections', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Company',
|
||||
name: 'Acme Inc',
|
||||
address: {
|
||||
__typename: 'Address',
|
||||
city: 'New York',
|
||||
country: 'USA',
|
||||
},
|
||||
employees: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '456',
|
||||
__typename: 'Person',
|
||||
name: {
|
||||
__typename: 'FullName',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
+127
-31
@@ -1,36 +1,132 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
import { isObjectRecordConnection } from '../isObjectRecordConnection';
|
||||
|
||||
describe('isObjectRecordConnection', () => {
|
||||
const relationDefinitionMap: { [K in RelationType]: boolean } = {
|
||||
[RelationType.ONE_TO_MANY]: true,
|
||||
[RelationType.MANY_TO_ONE]: false,
|
||||
};
|
||||
|
||||
it.each(Object.entries(relationDefinitionMap))(
|
||||
'.$relation',
|
||||
(relation, expected) => {
|
||||
const emptyRecord = {};
|
||||
const result = isObjectRecordConnection(
|
||||
it('should return true for valid connection with edges', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
type: relation,
|
||||
} as NonNullable<FieldMetadataItem['relation']>,
|
||||
emptyRecord,
|
||||
);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it('should throw on unknown relation direction', () => {
|
||||
const emptyRecord = {};
|
||||
expect(() =>
|
||||
isObjectRecordConnection(
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
{
|
||||
direction: 'UNKNOWN_TYPE',
|
||||
} as any,
|
||||
emptyRecord,
|
||||
),
|
||||
).toThrowError();
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
id: '456',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('person', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid connection with empty edges array', () => {
|
||||
const storeValue = {
|
||||
__typename: 'CompanyConnection',
|
||||
edges: [],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('company', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid connection without edges (optional)', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('person', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for incorrect __typename', () => {
|
||||
const storeValue = {
|
||||
__typename: 'WrongConnection',
|
||||
edges: [],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for incorrect edge __typename', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WrongEdge',
|
||||
node: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true regardless of node content', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
id: '123',
|
||||
name: 'John Doe',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('person', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for null value', () => {
|
||||
const result = isObjectRecordConnection('person', null);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for undefined value', () => {
|
||||
const result = isObjectRecordConnection('person', undefined);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for primitive value', () => {
|
||||
const result = isObjectRecordConnection('person', 'not an object');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle camelCase object names', () => {
|
||||
const storeValue = {
|
||||
__typename: 'CalendarEventConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'CalendarEventEdge',
|
||||
node: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('calendarEvent', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import { isObjectRecordConnectionWithRefs } from '../isObjectRecordConnectionWithRefs';
|
||||
|
||||
describe('isObjectRecordConnectionWithRefs', () => {
|
||||
it('should return true for valid connection with edges', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
__ref: 'Person:123',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
__ref: 'Person:456',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('person', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid connection with empty edges array', () => {
|
||||
const storeValue = {
|
||||
__typename: 'CompanyConnection',
|
||||
edges: [],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('company', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for connection without edges (required)', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for incorrect __typename', () => {
|
||||
const storeValue = {
|
||||
__typename: 'WrongConnection',
|
||||
edges: [],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for incorrect edge __typename', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WrongEdge',
|
||||
node: {
|
||||
__ref: 'Person:123',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for incorrect __ref prefix', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
__ref: 'Company:123',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for null value', () => {
|
||||
const result = isObjectRecordConnectionWithRefs('person', null);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for undefined value', () => {
|
||||
const result = isObjectRecordConnectionWithRefs('person', undefined);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for primitive value', () => {
|
||||
const result = isObjectRecordConnectionWithRefs('person', 'not an object');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle camelCase object names', () => {
|
||||
const storeValue = {
|
||||
__typename: 'CalendarEventConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'CalendarEventEdge',
|
||||
node: {
|
||||
__ref: 'CalendarEvent:123',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs(
|
||||
'calendarEvent',
|
||||
storeValue,
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when node is missing __ref', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
Vendored
+2
-2
@@ -2,7 +2,7 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
|
||||
import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename';
|
||||
import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo';
|
||||
import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
@@ -40,5 +40,5 @@ export const getRecordConnectionFromRecords = <T extends ObjectRecord>({
|
||||
}),
|
||||
...(withPageInfo && { pageInfo: getEmptyPageInfo() }),
|
||||
...(withPageInfo && { totalCount: records.length }),
|
||||
} as RecordGqlConnection;
|
||||
} as RecordGqlConnectionEdgesRequired;
|
||||
};
|
||||
|
||||
Vendored
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const getRecordsFromRecordConnection = <T extends ObjectRecord>({
|
||||
recordConnection,
|
||||
}: {
|
||||
recordConnection: RecordGqlConnection;
|
||||
recordConnection: RecordGqlConnectionEdgesRequired;
|
||||
}): T[] => {
|
||||
return recordConnection?.edges?.map((edge) =>
|
||||
getRecordFromRecordNode<T>({ recordNode: edge.node }),
|
||||
|
||||
+26
-16
@@ -1,20 +1,30 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type StoreValue } from '@apollo/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
export const isObjectRecordConnection = (
|
||||
relation: NonNullable<FieldMetadataItem['relation']>,
|
||||
value: unknown,
|
||||
): value is RecordGqlConnection => {
|
||||
switch (relation.type) {
|
||||
case RelationType.ONE_TO_MANY: {
|
||||
return true;
|
||||
}
|
||||
case RelationType.MANY_TO_ONE:
|
||||
return false;
|
||||
default: {
|
||||
return assertUnreachable(relation.type);
|
||||
}
|
||||
}
|
||||
objectNameSingular: string,
|
||||
storeValue: StoreValue,
|
||||
): storeValue is RecordGqlConnection => {
|
||||
const objectConnectionTypeName = `${capitalize(
|
||||
objectNameSingular,
|
||||
)}Connection`;
|
||||
const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`;
|
||||
const cachedObjectConnectionSchema = z.object({
|
||||
__typename: z.literal(objectConnectionTypeName),
|
||||
edges: z
|
||||
.array(
|
||||
z.object({
|
||||
__typename: z.literal(objectEdgeTypeName),
|
||||
node: z.object(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
const cachedConnectionValidation =
|
||||
cachedObjectConnectionSchema.safeParse(storeValue);
|
||||
|
||||
return cachedConnectionValidation.success;
|
||||
};
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ import { type Nullable } from 'twenty-ui/utilities';
|
||||
|
||||
export type RecordGqlConnection = {
|
||||
__typename?: string;
|
||||
edges: RecordGqlEdge[];
|
||||
pageInfo: {
|
||||
edges?: RecordGqlEdge[];
|
||||
pageInfo?: {
|
||||
__typename?: Nullable<string>;
|
||||
hasNextPage?: Nullable<boolean>;
|
||||
hasPreviousPage?: Nullable<boolean>;
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
|
||||
export type RecordGqlConnectionEdgesRequired = Omit<
|
||||
RecordGqlConnection,
|
||||
'edges'
|
||||
> &
|
||||
Required<Pick<RecordGqlConnection, 'edges'>>;
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
|
||||
export type RecordGqlOperationFindDuplicatesResult = {
|
||||
[objectNamePlural: string]: RecordGqlConnection[];
|
||||
[objectNamePlural: string]: RecordGqlConnectionEdgesRequired[];
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
|
||||
export type RecordGqlOperationFindManyResult = {
|
||||
[objectNamePlural: string]: RecordGqlConnection;
|
||||
[objectNamePlural: string]: RecordGqlConnectionEdgesRequired;
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
|
||||
export type RecordGqlOperationSearchResult = {
|
||||
[objectNamePlural: string]: RecordGqlConnection;
|
||||
[objectNamePlural: string]: RecordGqlConnectionEdgesRequired;
|
||||
};
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { peopleQueryResult } from '~/testing/mock-data/people';
|
||||
@@ -36,7 +36,7 @@ export const query = gql`
|
||||
|
||||
export const mockPageSize = 2;
|
||||
|
||||
export const peopleMockWithIdsOnly: RecordGqlConnection = {
|
||||
export const peopleMockWithIdsOnly: RecordGqlConnectionEdgesRequired = {
|
||||
...peopleQueryResult.people,
|
||||
edges: peopleQueryResult.people.edges.map((edge) => ({
|
||||
...edge,
|
||||
@@ -72,7 +72,7 @@ export const variablesThirdRequest = {
|
||||
};
|
||||
|
||||
const paginateRequestResponse = (
|
||||
response: RecordGqlConnection,
|
||||
response: RecordGqlConnectionEdgesRequired,
|
||||
start: number,
|
||||
end: number,
|
||||
hasNextPage: boolean,
|
||||
@@ -86,7 +86,7 @@ const paginateRequestResponse = (
|
||||
startCursor: response.edges[start].cursor,
|
||||
endCursor: response.edges[end].cursor,
|
||||
hasNextPage,
|
||||
} satisfies RecordGqlConnection['pageInfo'],
|
||||
} satisfies RecordGqlConnectionEdgesRequired['pageInfo'],
|
||||
totalCount,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { type ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
import { type RecordGqlOperationFindDuplicatesResult } from '@/object-record/graphql/types/RecordGqlOperationFindDuplicatesResults';
|
||||
import { useFindDuplicateRecordsQuery } from '@/object-record/hooks/useFindDuplicatesRecordsQuery';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
@@ -20,7 +20,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
skip,
|
||||
}: ObjectMetadataItemIdentifier & {
|
||||
objectRecordIds: string[] | undefined;
|
||||
onCompleted?: (data: RecordGqlConnection[]) => void;
|
||||
onCompleted?: (data: RecordGqlConnectionEdgesRequired[]) => void;
|
||||
skip?: boolean;
|
||||
}) => {
|
||||
const findDuplicateQueryStateIdentifier = objectNameSingular;
|
||||
@@ -69,7 +69,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
|
||||
const results = useMemo(
|
||||
() =>
|
||||
objectResults?.map((result: RecordGqlConnection) => {
|
||||
objectResults?.map((result: RecordGqlConnectionEdgesRequired) => {
|
||||
return result
|
||||
? (getRecordsFromRecordConnection({
|
||||
recordConnection: result,
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
|
||||
export type CombinedFindManyRecordsQueryResult = {
|
||||
[namePlural: string]: RecordGqlConnection;
|
||||
[namePlural: string]: RecordGqlConnectionEdgesRequired;
|
||||
};
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { type SearchRecord } from '~/generated-metadata/graphql';
|
||||
import { sortMorphItems } from '../sortMorphItems';
|
||||
|
||||
const createMorphItem = (
|
||||
recordId: string,
|
||||
isSelected: boolean,
|
||||
): RecordPickerPickableMorphItem => ({
|
||||
recordId,
|
||||
objectMetadataId: 'object-1',
|
||||
isSelected,
|
||||
isMatchingSearchFilter: true,
|
||||
});
|
||||
|
||||
const createSearchRecord = (recordId: string): SearchRecord => ({
|
||||
recordId,
|
||||
label: `Record ${recordId}`,
|
||||
objectNameSingular: 'person',
|
||||
tsRank: 0,
|
||||
tsRankCD: 0,
|
||||
});
|
||||
|
||||
describe('sortMorphItems', () => {
|
||||
it('should sort selected items before non-selected items', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [
|
||||
createMorphItem('1', false),
|
||||
createMorphItem('2', true),
|
||||
createMorphItem('3', false),
|
||||
];
|
||||
const searchRecords: SearchRecord[] = [
|
||||
createSearchRecord('1'),
|
||||
createSearchRecord('2'),
|
||||
createSearchRecord('3'),
|
||||
];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
expect(result[0].recordId).toBe('2');
|
||||
expect(result[0].isSelected).toBe(true);
|
||||
expect(result[1].isSelected).toBe(false);
|
||||
expect(result[2].isSelected).toBe(false);
|
||||
});
|
||||
|
||||
it('should sort by search record order within non-selected items', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [
|
||||
createMorphItem('3', false),
|
||||
createMorphItem('1', false),
|
||||
createMorphItem('2', false),
|
||||
];
|
||||
const searchRecords: SearchRecord[] = [
|
||||
createSearchRecord('1'),
|
||||
createSearchRecord('2'),
|
||||
createSearchRecord('3'),
|
||||
];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
expect(result.map((item) => item.recordId)).toEqual(['1', '2', '3']);
|
||||
});
|
||||
|
||||
it('should sort by search record order within selected items', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [
|
||||
createMorphItem('3', true),
|
||||
createMorphItem('1', true),
|
||||
createMorphItem('2', true),
|
||||
];
|
||||
const searchRecords: SearchRecord[] = [
|
||||
createSearchRecord('1'),
|
||||
createSearchRecord('2'),
|
||||
createSearchRecord('3'),
|
||||
];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
expect(result.map((item) => item.recordId)).toEqual(['1', '2', '3']);
|
||||
});
|
||||
|
||||
it('should handle mixed selected and non-selected items with correct ordering', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [
|
||||
createMorphItem('4', false),
|
||||
createMorphItem('2', true),
|
||||
createMorphItem('1', false),
|
||||
createMorphItem('3', true),
|
||||
];
|
||||
const searchRecords: SearchRecord[] = [
|
||||
createSearchRecord('1'),
|
||||
createSearchRecord('2'),
|
||||
createSearchRecord('3'),
|
||||
createSearchRecord('4'),
|
||||
];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
expect(result.map((item) => item.recordId)).toEqual(['2', '3', '1', '4']);
|
||||
expect(result[0].isSelected).toBe(true);
|
||||
expect(result[1].isSelected).toBe(true);
|
||||
expect(result[2].isSelected).toBe(false);
|
||||
expect(result[3].isSelected).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle empty morphItems array', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [];
|
||||
const searchRecords: SearchRecord[] = [createSearchRecord('1')];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle empty searchRecords array', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [
|
||||
createMorphItem('1', false),
|
||||
createMorphItem('2', true),
|
||||
];
|
||||
const searchRecords: SearchRecord[] = [];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
expect(result[0].recordId).toBe('2');
|
||||
expect(result[0].isSelected).toBe(true);
|
||||
});
|
||||
|
||||
it('should place items not present in searchRecords before indexed items', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [
|
||||
createMorphItem('unknown', false),
|
||||
createMorphItem('1', false),
|
||||
];
|
||||
const searchRecords: SearchRecord[] = [createSearchRecord('1')];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
// Items not in searchRecords get rank -1, so they come before items with rank >= 0
|
||||
expect(result[0].recordId).toBe('unknown');
|
||||
expect(result[1].recordId).toBe('1');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user