Add objectRecordCounts query to /metadata endpoint (#18054)
## Summary - Adds an `objectRecordCounts` query on the `/metadata` GraphQL endpoint that returns approximate record counts for all objects in the workspace - Uses PostgreSQL's `pg_class.reltuples` catalog stats — a single instant query instead of N `COUNT(*)` table scans - Replaces the previous `CombinedFindManyRecords` approach which hit the server's 20 root resolver limit and silently showed 0 for all counts on the settings Data Model page ### Server - `ObjectRecordCountDTO` — GraphQL type with `objectNamePlural` and `totalCount` - `ObjectRecordCountService` — reads `pg_class` catalog for the workspace schema - Query added to `ObjectMetadataResolver` with `@MetadataResolver()` + `NoPermissionGuard` ### Frontend - `OBJECT_RECORD_COUNTS` query added to `object-metadata/graphql/queries.ts` - `useCombinedGetTotalCount` simplified to a zero-argument hook using the new query - `SettingsObjectTable` simplified to a single hook call --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3378,6 +3378,12 @@ export type ObjectPermissionInput = {
|
||||
objectMetadataId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type ObjectRecordCount = {
|
||||
__typename?: 'ObjectRecordCount';
|
||||
objectNamePlural: Scalars['String'];
|
||||
totalCount: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type ObjectRecordEvent = {
|
||||
__typename?: 'ObjectRecordEvent';
|
||||
action: DatabaseEventAction;
|
||||
@@ -3751,6 +3757,7 @@ export type Query = {
|
||||
navigationMenuItem?: Maybe<NavigationMenuItem>;
|
||||
navigationMenuItems: Array<NavigationMenuItem>;
|
||||
object: Object;
|
||||
objectRecordCounts: Array<ObjectRecordCount>;
|
||||
objects: ObjectConnection;
|
||||
pieChartData: PieChartDataOutput;
|
||||
skill?: Maybe<Skill>;
|
||||
@@ -5956,6 +5963,11 @@ export type ObjectMetadataItemsQueryVariables = Exact<{ [key: string]: never; }>
|
||||
|
||||
export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'ObjectEdge', node: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } };
|
||||
|
||||
export type ObjectRecordCountsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ObjectRecordCountsQuery = { __typename?: 'Query', objectRecordCounts: Array<{ __typename?: 'ObjectRecordCount', objectNamePlural: string, totalCount: number }> };
|
||||
|
||||
export type SkipBookOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
@@ -11442,6 +11454,41 @@ export function useObjectMetadataItemsLazyQuery(baseOptions?: Apollo.LazyQueryHo
|
||||
export type ObjectMetadataItemsQueryHookResult = ReturnType<typeof useObjectMetadataItemsQuery>;
|
||||
export type ObjectMetadataItemsLazyQueryHookResult = ReturnType<typeof useObjectMetadataItemsLazyQuery>;
|
||||
export type ObjectMetadataItemsQueryResult = Apollo.QueryResult<ObjectMetadataItemsQuery, ObjectMetadataItemsQueryVariables>;
|
||||
export const ObjectRecordCountsDocument = gql`
|
||||
query ObjectRecordCounts {
|
||||
objectRecordCounts {
|
||||
objectNamePlural
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useObjectRecordCountsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useObjectRecordCountsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useObjectRecordCountsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useObjectRecordCountsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useObjectRecordCountsQuery(baseOptions?: Apollo.QueryHookOptions<ObjectRecordCountsQuery, ObjectRecordCountsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ObjectRecordCountsQuery, ObjectRecordCountsQueryVariables>(ObjectRecordCountsDocument, options);
|
||||
}
|
||||
export function useObjectRecordCountsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ObjectRecordCountsQuery, ObjectRecordCountsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ObjectRecordCountsQuery, ObjectRecordCountsQueryVariables>(ObjectRecordCountsDocument, options);
|
||||
}
|
||||
export type ObjectRecordCountsQueryHookResult = ReturnType<typeof useObjectRecordCountsQuery>;
|
||||
export type ObjectRecordCountsLazyQueryHookResult = ReturnType<typeof useObjectRecordCountsLazyQuery>;
|
||||
export type ObjectRecordCountsQueryResult = Apollo.QueryResult<ObjectRecordCountsQuery, ObjectRecordCountsQueryVariables>;
|
||||
export const SkipBookOnboardingStepDocument = gql`
|
||||
mutation SkipBookOnboardingStep {
|
||||
skipBookOnboardingStep {
|
||||
|
||||
@@ -19,3 +19,12 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const OBJECT_RECORD_COUNTS = gql`
|
||||
query ObjectRecordCounts {
|
||||
objectRecordCounts {
|
||||
objectNamePlural
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+19
-40
@@ -1,48 +1,27 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
||||
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
|
||||
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
||||
import { type CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||
import { OBJECT_RECORD_COUNTS } from '@/object-metadata/graphql/queries';
|
||||
|
||||
export const useCombinedGetTotalCount = ({
|
||||
objectMetadataItems,
|
||||
skip = false,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
skip?: boolean;
|
||||
}) => {
|
||||
const operationSignatures = objectMetadataItems.map(
|
||||
(objectMetadataItem) =>
|
||||
({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
variables: {},
|
||||
fields: {
|
||||
id: true,
|
||||
},
|
||||
}) satisfies RecordGqlOperationSignature,
|
||||
);
|
||||
type ObjectRecordCountsResult = {
|
||||
objectRecordCounts: {
|
||||
objectNamePlural: string;
|
||||
totalCount: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
const findManyQuery = useGenerateCombinedFindManyRecordsQuery({
|
||||
operationSignatures,
|
||||
});
|
||||
export const useCombinedGetTotalCount = () => {
|
||||
const { data } = useQuery<ObjectRecordCountsResult>(OBJECT_RECORD_COUNTS);
|
||||
|
||||
const { data } = useQuery<CombinedFindManyRecordsQueryResult>(
|
||||
findManyQuery ?? EMPTY_QUERY,
|
||||
{
|
||||
skip,
|
||||
client: apolloCoreClient,
|
||||
},
|
||||
);
|
||||
|
||||
const totalCountByObjectMetadataItemNamePlural = Object.fromEntries(
|
||||
Object.entries(data ?? {}).map(([namePlural, objectRecordConnection]) => [
|
||||
namePlural,
|
||||
objectRecordConnection.totalCount,
|
||||
]),
|
||||
const totalCountByObjectMetadataItemNamePlural = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
(data?.objectRecordCounts ?? []).map((item) => [
|
||||
item.objectNamePlural,
|
||||
item.totalCount,
|
||||
]),
|
||||
),
|
||||
[data],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -58,11 +58,8 @@ export const SettingsObjectTable = ({
|
||||
|
||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||
|
||||
const { totalCountByObjectMetadataItemNamePlural } = useCombinedGetTotalCount(
|
||||
{
|
||||
objectMetadataItems,
|
||||
},
|
||||
);
|
||||
const { totalCountByObjectMetadataItemNamePlural } =
|
||||
useCombinedGetTotalCount();
|
||||
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user