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:
Félix Malfait
2026-02-19 10:02:31 +01:00
committed by GitHub
parent a14b0ab6ca
commit 082400f751
9 changed files with 250 additions and 47 deletions
@@ -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 {