Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98d3232083 | ||
|
|
082400f751 |
@@ -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 {
|
||||
|
||||
+11
-5
@@ -58,11 +58,17 @@ export const processGroupByConnectionWithRecords = ({
|
||||
let totalCountDelta = 0;
|
||||
|
||||
for (const record of records) {
|
||||
const recordMatchesFilter = isRecordMatchingFilter({
|
||||
record,
|
||||
filter: queryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
let recordMatchesFilter: boolean;
|
||||
|
||||
try {
|
||||
recordMatchesFilter = isRecordMatchingFilter({
|
||||
record,
|
||||
filter: queryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
} catch {
|
||||
recordMatchesFilter = false;
|
||||
}
|
||||
|
||||
const belongsToGroup = doesRecordBelongToGroup(
|
||||
record,
|
||||
|
||||
+11
-5
@@ -118,11 +118,17 @@ export const triggerUpdateGroupByQueriesOptimisticEffect = ({
|
||||
> = new Map();
|
||||
|
||||
for (const record of records) {
|
||||
const recordMatchesFilter = isRecordMatchingFilter({
|
||||
record,
|
||||
filter: queryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
let recordMatchesFilter: boolean;
|
||||
|
||||
try {
|
||||
recordMatchesFilter = isRecordMatchingFilter({
|
||||
record,
|
||||
filter: queryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
} catch {
|
||||
recordMatchesFilter = false;
|
||||
}
|
||||
|
||||
if (
|
||||
shouldMatchRootQueryFilter &&
|
||||
|
||||
+11
-7
@@ -136,14 +136,18 @@ export const triggerCreateRecordsOptimisticEffect = ({
|
||||
isDefined(rootQueryFilter) &&
|
||||
shouldMatchRootQueryFilter === true
|
||||
) {
|
||||
const recordToCreateMatchesThisRootQueryFilter =
|
||||
isRecordMatchingFilter({
|
||||
record: recordToCreate,
|
||||
filter: rootQueryFilter,
|
||||
objectMetadataItem,
|
||||
});
|
||||
try {
|
||||
const recordToCreateMatchesThisRootQueryFilter =
|
||||
isRecordMatchingFilter({
|
||||
record: recordToCreate,
|
||||
filter: rootQueryFilter,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (!recordToCreateMatchesThisRootQueryFilter) {
|
||||
if (!recordToCreateMatchesThisRootQueryFilter) {
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-8
@@ -57,14 +57,20 @@ export const triggerDestroyRecordsOptimisticEffect = ({
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
|
||||
const recordsMatchingRootQueryFilter = recordsToDestroy.filter(
|
||||
(record) =>
|
||||
isRecordMatchingFilter({
|
||||
record,
|
||||
filter: rootQueryVariables?.filter ?? {},
|
||||
objectMetadataItem,
|
||||
}),
|
||||
);
|
||||
let recordsMatchingRootQueryFilter: RecordGqlNode[];
|
||||
|
||||
try {
|
||||
recordsMatchingRootQueryFilter = recordsToDestroy.filter(
|
||||
(record) =>
|
||||
isRecordMatchingFilter({
|
||||
record,
|
||||
filter: rootQueryVariables?.filter ?? {},
|
||||
objectMetadataItem,
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
recordsMatchingRootQueryFilter = [];
|
||||
}
|
||||
|
||||
const newTotalCount = isDefined(totalCount)
|
||||
? Math.max(totalCount - recordsMatchingRootQueryFilter.length, 0)
|
||||
|
||||
+17
-10
@@ -76,17 +76,24 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
const rootQueryFilter = rootQueryVariables?.filter;
|
||||
const rootQueryOrderBy = rootQueryVariables?.orderBy;
|
||||
|
||||
const updatedRecordMatchesThisRootQueryFilter = isRecordMatchingFilter({
|
||||
record: updatedRecord,
|
||||
filter: rootQueryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
let updatedRecordMatchesThisRootQueryFilter: boolean;
|
||||
let currentRecordIndexInRootQueryEdges: boolean;
|
||||
|
||||
const currentRecordIndexInRootQueryEdges = isRecordMatchingFilter({
|
||||
record: currentRecord,
|
||||
filter: rootQueryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
try {
|
||||
updatedRecordMatchesThisRootQueryFilter = isRecordMatchingFilter({
|
||||
record: updatedRecord,
|
||||
filter: rootQueryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
currentRecordIndexInRootQueryEdges = isRecordMatchingFilter({
|
||||
record: currentRecord,
|
||||
filter: rootQueryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
} catch {
|
||||
return rootQueryCachedResponse;
|
||||
}
|
||||
|
||||
const totalCount = readField<number | undefined>(
|
||||
'totalCount',
|
||||
|
||||
+12
-6
@@ -80,12 +80,18 @@ export const triggerUpdateRecordOptimisticEffectByBatch = ({
|
||||
const rootQueryOrderBy = rootQueryVariables?.orderBy;
|
||||
|
||||
for (const updatedRecord of updatedRecords) {
|
||||
const updatedRecordMatchesThisRootQueryFilter =
|
||||
isRecordMatchingFilter({
|
||||
record: updatedRecord,
|
||||
filter: rootQueryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
let updatedRecordMatchesThisRootQueryFilter: boolean;
|
||||
|
||||
try {
|
||||
updatedRecordMatchesThisRootQueryFilter =
|
||||
isRecordMatchingFilter({
|
||||
record: updatedRecord,
|
||||
filter: rootQueryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const updatedRecordFoundInRootQueryEdges = isDefined(
|
||||
rootQueryCurrentEdges.find(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType('ObjectRecordCount')
|
||||
export class ObjectRecordCountDTO {
|
||||
@Field(() => String)
|
||||
objectNamePlural: string;
|
||||
|
||||
@Field(() => Int)
|
||||
totalCount: number;
|
||||
}
|
||||
+2
@@ -25,6 +25,7 @@ import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadat
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { ObjectRecordCountService } from 'src/engine/metadata-modules/object-metadata/object-record-count.service';
|
||||
import { ObjectMetadataToolsFactory } from 'src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
@@ -93,6 +94,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
providers: [
|
||||
ObjectMetadataService,
|
||||
ObjectMetadataResolver,
|
||||
ObjectRecordCountService,
|
||||
ObjectMetadataToolsFactory,
|
||||
],
|
||||
exports: [ObjectMetadataService, ObjectMetadataToolsFactory],
|
||||
|
||||
+20
-2
@@ -1,8 +1,16 @@
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import { Args, Context, Mutation, Parent, ResolveField } from '@nestjs/graphql';
|
||||
import {
|
||||
Args,
|
||||
Context,
|
||||
Mutation,
|
||||
Parent,
|
||||
Query,
|
||||
ResolveField,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
@@ -10,7 +18,6 @@ import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.typ
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
@@ -19,8 +26,10 @@ import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dto
|
||||
import { CreateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import { ObjectRecordCountDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-record-count.dto';
|
||||
import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { ObjectRecordCountService } from 'src/engine/metadata-modules/object-metadata/object-record-count.service';
|
||||
import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util';
|
||||
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
@@ -35,9 +44,18 @@ import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-module
|
||||
export class ObjectMetadataResolver {
|
||||
constructor(
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly objectRecordCountService: ObjectRecordCountService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL))
|
||||
@Query(() => [ObjectRecordCountDTO])
|
||||
async objectRecordCounts(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<ObjectRecordCountDTO[]> {
|
||||
return this.objectRecordCountService.getRecordCounts(workspaceId);
|
||||
}
|
||||
|
||||
@ResolveField(() => String, { nullable: true })
|
||||
async labelPlural(
|
||||
@Parent() objectMetadata: ObjectMetadataDTO,
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type ObjectRecordCountDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-record-count.dto';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectRecordCountService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
async getRecordCounts(workspaceId: string): Promise<ObjectRecordCountDTO[]> {
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatObjectMetadatas = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
).filter(isDefined);
|
||||
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
const dataSource =
|
||||
await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
|
||||
|
||||
const rows: { relname: string; approximate_count: number }[] =
|
||||
await dataSource.query(
|
||||
`SELECT relname, reltuples::bigint AS approximate_count
|
||||
FROM pg_class c
|
||||
JOIN pg_namespace n ON c.relnamespace = n.oid
|
||||
WHERE n.nspname = $1
|
||||
AND c.relkind = 'r'`,
|
||||
[schemaName],
|
||||
undefined,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const countByTableName = new Map<string, number>();
|
||||
|
||||
for (const row of rows) {
|
||||
countByTableName.set(
|
||||
row.relname,
|
||||
Math.max(0, Number(row.approximate_count)),
|
||||
);
|
||||
}
|
||||
|
||||
return flatObjectMetadatas.map((flatObjectMetadata) => ({
|
||||
objectNamePlural: flatObjectMetadata.namePlural,
|
||||
totalCount:
|
||||
countByTableName.get(
|
||||
computeTableName(
|
||||
flatObjectMetadata.nameSingular,
|
||||
flatObjectMetadata.isCustom,
|
||||
),
|
||||
) ?? 0,
|
||||
}));
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
|
||||
const OBJECT_RECORD_COUNTS_QUERY = gql`
|
||||
query ObjectRecordCounts {
|
||||
objectRecordCounts {
|
||||
objectNamePlural
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('objectRecordCounts', () => {
|
||||
it('should return counts for all workspace objects', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: OBJECT_RECORD_COUNTS_QUERY,
|
||||
});
|
||||
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.objectRecordCounts).toBeDefined();
|
||||
|
||||
const counts: { objectNamePlural: string; totalCount: number }[] =
|
||||
response.body.data.objectRecordCounts;
|
||||
|
||||
expect(Array.isArray(counts)).toBe(true);
|
||||
expect(counts.length).toBeGreaterThan(0);
|
||||
|
||||
for (const entry of counts) {
|
||||
expect(typeof entry.objectNamePlural).toBe('string');
|
||||
expect(entry.objectNamePlural.length).toBeGreaterThan(0);
|
||||
expect(typeof entry.totalCount).toBe('number');
|
||||
expect(entry.totalCount).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should include well-known standard objects', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: OBJECT_RECORD_COUNTS_QUERY,
|
||||
});
|
||||
|
||||
const counts: { objectNamePlural: string; totalCount: number }[] =
|
||||
response.body.data.objectRecordCounts;
|
||||
|
||||
const objectNames = counts.map((entry) => entry.objectNamePlural);
|
||||
|
||||
expect(objectNames).toContain('people');
|
||||
expect(objectNames).toContain('companies');
|
||||
expect(objectNames).toContain('opportunities');
|
||||
});
|
||||
|
||||
it('should return non-negative totalCount values', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: OBJECT_RECORD_COUNTS_QUERY,
|
||||
});
|
||||
|
||||
const counts: { objectNamePlural: string; totalCount: number }[] =
|
||||
response.body.data.objectRecordCounts;
|
||||
|
||||
const negativeEntries = counts.filter((entry) => entry.totalCount < 0);
|
||||
|
||||
expect(negativeEntries).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should reject requests with an invalid token', async () => {
|
||||
const response = await makeMetadataAPIRequest(
|
||||
{ query: OBJECT_RECORD_COUNTS_QUERY },
|
||||
INVALID_ACCESS_TOKEN,
|
||||
);
|
||||
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user