Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 087a4e384d | |||
| 4c93c90ea1 |
@@ -1244,6 +1244,7 @@ export enum FeatureFlagKey {
|
||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
|
||||
IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED',
|
||||
IS_COMMON_API_ENABLED = 'IS_COMMON_API_ENABLED',
|
||||
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
|
||||
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED',
|
||||
|
||||
@@ -1208,6 +1208,7 @@ export enum FeatureFlagKey {
|
||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
|
||||
IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED',
|
||||
IS_COMMON_API_ENABLED = 'IS_COMMON_API_ENABLED',
|
||||
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
|
||||
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED',
|
||||
|
||||
+11
-4
@@ -1,6 +1,5 @@
|
||||
import { FavoriteFolderNavigationDrawerItemDropdown } from '@/favorites/components/FavoriteFolderNavigationDrawerItemDropdown';
|
||||
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
|
||||
import { FavoriteNavigationDrawerSubItem } from '@/favorites/components/FavoriteNavigationDrawerSubItem';
|
||||
import { FavoritesDroppable } from '@/favorites/components/FavoritesDroppable';
|
||||
import { FAVORITE_FOLDER_DELETE_MODAL_ID } from '@/favorites/constants/FavoriteFolderDeleteModalId';
|
||||
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
|
||||
@@ -8,8 +7,10 @@ import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useDeleteFavoriteFolder } from '@/favorites/hooks/useDeleteFavoriteFolder';
|
||||
import { useRenameFavoriteFolder } from '@/favorites/hooks/useRenameFavoriteFolder';
|
||||
import { openFavoriteFolderIdsState } from '@/favorites/states/openFavoriteFolderIdsState';
|
||||
import { getFavoriteSecondaryLabel } from '@/favorites/utils/getFavoriteSecondaryLabel';
|
||||
import { isLocationMatchingFavorite } from '@/favorites/utils/isLocationMatchingFavorite';
|
||||
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
@@ -19,13 +20,14 @@ import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpe
|
||||
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { useContext, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
@@ -43,6 +45,7 @@ export const CurrentWorkspaceMemberFavorites = ({
|
||||
folder,
|
||||
isGroup,
|
||||
}: CurrentWorkspaceMemberFavoritesProps) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const currentPath = useLocation().pathname;
|
||||
const currentViewPath = useLocation().pathname + useLocation().search;
|
||||
const { isDragging } = useContext(FavoritesDragContext);
|
||||
@@ -196,8 +199,12 @@ export const CurrentWorkspaceMemberFavorites = ({
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
itemComponent={
|
||||
<FavoriteNavigationDrawerSubItem
|
||||
favorite={favorite}
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={getFavoriteSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
favoriteObjectNameSingular:
|
||||
favorite.objectNameSingular,
|
||||
})}
|
||||
label={favorite.labelIdentifier}
|
||||
Icon={() => <FavoriteIcon favorite={favorite} />}
|
||||
to={isDragging ? undefined : favorite.link}
|
||||
|
||||
+10
-3
@@ -1,14 +1,17 @@
|
||||
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
|
||||
import { FavoriteNavigationDrawerItem } from '@/favorites/components/FavoriteNavigationDrawerItem';
|
||||
import { FavoritesDroppable } from '@/favorites/components/FavoritesDroppable';
|
||||
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { getFavoriteSecondaryLabel } from '@/favorites/utils/getFavoriteSecondaryLabel';
|
||||
import { isLocationMatchingFavorite } from '@/favorites/utils/isLocationMatchingFavorite';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconHeartOff } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
|
||||
@@ -21,6 +24,7 @@ const StyledOrphanFavoritesContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const CurrentWorkspaceMemberOrphanFavorites = () => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
const currentPath = useLocation().pathname;
|
||||
@@ -42,8 +46,11 @@ export const CurrentWorkspaceMemberOrphanFavorites = () => {
|
||||
isInsideScrollableContainer={true}
|
||||
itemComponent={
|
||||
<StyledOrphanFavoritesContainer>
|
||||
<FavoriteNavigationDrawerItem
|
||||
favorite={favorite}
|
||||
<NavigationDrawerItem
|
||||
secondaryLabel={getFavoriteSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
favoriteObjectNameSingular: favorite.objectNameSingular,
|
||||
})}
|
||||
label={favorite.labelIdentifier}
|
||||
Icon={() => <FavoriteIcon favorite={favorite} />}
|
||||
active={isLocationMatchingFavorite(
|
||||
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
|
||||
type FavoriteNavigationDrawerItemProps = {
|
||||
favorite: ProcessedFavorite;
|
||||
label: string;
|
||||
Icon?: React.ComponentProps<typeof NavigationDrawerItem>['Icon'];
|
||||
to?: string;
|
||||
active?: boolean;
|
||||
rightOptions?: React.ComponentProps<
|
||||
typeof NavigationDrawerItem
|
||||
>['rightOptions'];
|
||||
isDragging?: boolean;
|
||||
triggerEvent?: React.ComponentProps<
|
||||
typeof NavigationDrawerItem
|
||||
>['triggerEvent'];
|
||||
};
|
||||
|
||||
export const FavoriteNavigationDrawerItem = ({
|
||||
favorite,
|
||||
label,
|
||||
Icon,
|
||||
to,
|
||||
active,
|
||||
rightOptions,
|
||||
isDragging,
|
||||
triggerEvent,
|
||||
}: FavoriteNavigationDrawerItemProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: favorite.objectNameSingular,
|
||||
});
|
||||
|
||||
if (!favorite.objectNameSingular) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationDrawerItem
|
||||
label={label}
|
||||
secondaryLabel={objectMetadataItem.labelSingular}
|
||||
Icon={Icon}
|
||||
to={to}
|
||||
active={active}
|
||||
rightOptions={rightOptions}
|
||||
isDragging={isDragging}
|
||||
triggerEvent={triggerEvent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
|
||||
type FavoriteNavigationDrawerSubItemProps = {
|
||||
favorite: ProcessedFavorite;
|
||||
label: string;
|
||||
Icon?: React.ComponentProps<typeof NavigationDrawerSubItem>['Icon'];
|
||||
to?: string;
|
||||
active?: boolean;
|
||||
subItemState?: React.ComponentProps<
|
||||
typeof NavigationDrawerSubItem
|
||||
>['subItemState'];
|
||||
rightOptions?: React.ComponentProps<
|
||||
typeof NavigationDrawerSubItem
|
||||
>['rightOptions'];
|
||||
isDragging?: boolean;
|
||||
triggerEvent?: React.ComponentProps<
|
||||
typeof NavigationDrawerSubItem
|
||||
>['triggerEvent'];
|
||||
};
|
||||
|
||||
export const FavoriteNavigationDrawerSubItem = ({
|
||||
favorite,
|
||||
label,
|
||||
Icon,
|
||||
to,
|
||||
active,
|
||||
subItemState,
|
||||
rightOptions,
|
||||
isDragging,
|
||||
triggerEvent,
|
||||
}: FavoriteNavigationDrawerSubItemProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: favorite.objectNameSingular || '',
|
||||
});
|
||||
|
||||
if (!favorite.objectNameSingular) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationDrawerSubItem
|
||||
label={label}
|
||||
secondaryLabel={objectMetadataItem.labelSingular}
|
||||
Icon={Icon}
|
||||
to={to}
|
||||
active={active}
|
||||
subItemState={subItemState}
|
||||
rightOptions={rightOptions}
|
||||
isDragging={isDragging}
|
||||
triggerEvent={triggerEvent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
import { getFavoriteSecondaryLabel } from '../getFavoriteSecondaryLabel';
|
||||
|
||||
describe('getFavoriteSecondaryLabel', () => {
|
||||
it('should return "View" for view object', () => {
|
||||
const result = getFavoriteSecondaryLabel({
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
favoriteObjectNameSingular: 'view',
|
||||
});
|
||||
|
||||
expect(result).toBe('View');
|
||||
});
|
||||
|
||||
it('should return labelSingular for matching object metadata item', () => {
|
||||
const result = getFavoriteSecondaryLabel({
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
favoriteObjectNameSingular: 'person',
|
||||
});
|
||||
|
||||
expect(result).toBe('Person');
|
||||
});
|
||||
|
||||
it('should return undefined when object metadata item is not found', () => {
|
||||
const result = getFavoriteSecondaryLabel({
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
favoriteObjectNameSingular: 'nonexistent',
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
type GetFavoriteSecondaryLabelProps = {
|
||||
objectMetadataItems: Pick<
|
||||
ObjectMetadataItem,
|
||||
'nameSingular' | 'labelSingular'
|
||||
>[];
|
||||
favoriteObjectNameSingular: string;
|
||||
};
|
||||
|
||||
export const getFavoriteSecondaryLabel = ({
|
||||
objectMetadataItems,
|
||||
favoriteObjectNameSingular,
|
||||
}: GetFavoriteSecondaryLabelProps) => {
|
||||
if (favoriteObjectNameSingular === 'view') {
|
||||
return 'View';
|
||||
}
|
||||
|
||||
return objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === favoriteObjectNameSingular,
|
||||
)?.labelSingular;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type PageInfo } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { type PageInfo } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
export type CommonPageInfo = {
|
||||
hasNextPage: NonNullable<PageInfo['hasNextPage']>;
|
||||
|
||||
+33
@@ -4,6 +4,20 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||
import { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper';
|
||||
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
|
||||
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
|
||||
import { GraphqlQueryCreateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service';
|
||||
import { GraphqlQueryDeleteManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service';
|
||||
import { GraphqlQueryDeleteOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service';
|
||||
import { GraphqlQueryDestroyManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service';
|
||||
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
|
||||
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
|
||||
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
|
||||
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
|
||||
import { GraphqlQueryMergeManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service';
|
||||
import { GraphqlQueryRestoreManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service';
|
||||
import { GraphqlQueryRestoreOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service';
|
||||
import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service';
|
||||
import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service';
|
||||
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
|
||||
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
@@ -14,6 +28,23 @@ import { ViewFilterGroupModule } from 'src/engine/metadata-modules/view-filter-g
|
||||
import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module';
|
||||
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||
|
||||
const graphqlQueryResolvers = [
|
||||
GraphqlQueryCreateManyResolverService,
|
||||
GraphqlQueryCreateOneResolverService,
|
||||
GraphqlQueryDeleteManyResolverService,
|
||||
GraphqlQueryDeleteOneResolverService,
|
||||
GraphqlQueryDestroyManyResolverService,
|
||||
GraphqlQueryDestroyOneResolverService,
|
||||
GraphqlQueryFindDuplicatesResolverService,
|
||||
GraphqlQueryFindManyResolverService,
|
||||
GraphqlQueryFindOneResolverService,
|
||||
GraphqlQueryMergeManyResolverService,
|
||||
GraphqlQueryRestoreManyResolverService,
|
||||
GraphqlQueryRestoreOneResolverService,
|
||||
GraphqlQueryUpdateManyResolverService,
|
||||
GraphqlQueryUpdateOneResolverService,
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceQueryHookModule,
|
||||
@@ -30,6 +61,8 @@ import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||
ProcessNestedRelationsHelper,
|
||||
ProcessNestedRelationsV2Helper,
|
||||
ProcessAggregateHelper,
|
||||
...graphqlQueryResolvers,
|
||||
],
|
||||
exports: [...graphqlQueryResolvers],
|
||||
})
|
||||
export class GraphqlQueryRunnerModule {}
|
||||
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { type ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import {
|
||||
type ResolverArgs,
|
||||
type WorkspaceResolverBuilderMethodNames,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
|
||||
import { type GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
|
||||
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory';
|
||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export type GraphqlQueryResolverExecutionArgs<Input extends ResolverArgs> = {
|
||||
args: Input;
|
||||
options: WorkspaceQueryRunnerOptions;
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
repository: WorkspaceRepository<ObjectLiteral>;
|
||||
graphqlQueryParser: GraphqlQueryParser;
|
||||
graphqlQuerySelectedFieldsResult: GraphqlQuerySelectedFieldsResult;
|
||||
isExecutedByApiKey: boolean;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export abstract class GraphqlQueryBaseResolverService<
|
||||
Input extends ResolverArgs,
|
||||
Response extends
|
||||
| ObjectRecord
|
||||
| ObjectRecord[]
|
||||
| IConnection<ObjectRecord, IEdge<ObjectRecord>>
|
||||
| IConnection<ObjectRecord, IEdge<ObjectRecord>>[],
|
||||
> {
|
||||
@Inject()
|
||||
protected readonly workspaceQueryHookService: WorkspaceQueryHookService;
|
||||
@Inject()
|
||||
protected readonly queryRunnerArgsFactory: QueryRunnerArgsFactory;
|
||||
@Inject()
|
||||
protected readonly queryResultGettersFactory: QueryResultGettersFactory;
|
||||
@Inject()
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||
@Inject()
|
||||
protected readonly processNestedRelationsHelper: ProcessNestedRelationsHelper;
|
||||
@Inject()
|
||||
protected readonly permissionsService: PermissionsService;
|
||||
@Inject()
|
||||
protected readonly userRoleService: UserRoleService;
|
||||
@Inject()
|
||||
protected readonly apiKeyRoleService: ApiKeyRoleService;
|
||||
|
||||
public async execute(
|
||||
args: Input,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
operationName: WorkspaceResolverBuilderMethodNames,
|
||||
): Promise<Response | undefined> {
|
||||
try {
|
||||
const { authContext, objectMetadataItemWithFieldMaps } = options;
|
||||
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
assertIsDefinedOrThrow(workspace);
|
||||
|
||||
await this.validate(args, options);
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||
|
||||
if (objectMetadataItemWithFieldMaps.isSystem === true) {
|
||||
await this.validateSettingsPermissionsOnObjectOrThrow(options);
|
||||
}
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
operationName,
|
||||
args,
|
||||
);
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
hookedArgs,
|
||||
options,
|
||||
operationName,
|
||||
)) as Input;
|
||||
|
||||
let roleId: string | undefined;
|
||||
|
||||
if (isDefined(authContext.apiKey)) {
|
||||
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
|
||||
authContext.apiKey.id,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(authContext.userWorkspaceId)) {
|
||||
roleId = await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
if (!roleId) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!isDefined(authContext.apiKey) &&
|
||||
!isDefined(authContext.userWorkspaceId)
|
||||
) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.NO_AUTHENTICATION_CONTEXT,
|
||||
PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT,
|
||||
);
|
||||
}
|
||||
const rolePermissionConfig: RolePermissionConfig | undefined = roleId
|
||||
? { unionOf: [roleId] }
|
||||
: undefined;
|
||||
const repository = workspaceDataSource.getRepository(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
rolePermissionConfig,
|
||||
authContext,
|
||||
);
|
||||
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
options.objectMetadataMaps,
|
||||
);
|
||||
|
||||
const selectedFields = graphqlFields(options.info);
|
||||
|
||||
const graphqlQuerySelectedFieldsResult =
|
||||
graphqlQueryParser.parseSelectedFields(selectedFields);
|
||||
|
||||
const graphqlQueryResolverExecutionArgs = {
|
||||
args: computedArgs,
|
||||
options,
|
||||
workspaceDataSource,
|
||||
repository,
|
||||
graphqlQueryParser,
|
||||
graphqlQuerySelectedFieldsResult,
|
||||
isExecutedByApiKey: isDefined(authContext.apiKey),
|
||||
rolePermissionConfig,
|
||||
};
|
||||
const results = await this.resolve(
|
||||
graphqlQueryResolverExecutionArgs,
|
||||
featureFlagsMap,
|
||||
);
|
||||
|
||||
const resultWithGetters = await this.queryResultGettersFactory.create(
|
||||
results,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
workspace.id,
|
||||
options.objectMetadataMaps,
|
||||
);
|
||||
|
||||
await this.workspaceQueryHookService.executePostQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
operationName,
|
||||
resultWithGetters,
|
||||
);
|
||||
|
||||
return resultWithGetters;
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
private async validateSettingsPermissionsOnObjectOrThrow(
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
) {
|
||||
const { authContext, objectMetadataItemWithFieldMaps } = options;
|
||||
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
|
||||
|
||||
if (
|
||||
Object.keys(OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS).includes(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
)
|
||||
) {
|
||||
const permissionRequired: PermissionFlagType =
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS[
|
||||
objectMetadataItemWithFieldMaps.nameSingular
|
||||
];
|
||||
|
||||
const userHasPermission =
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
setting: permissionRequired,
|
||||
workspaceId: workspace.id,
|
||||
apiKeyId: authContext.apiKey?.id,
|
||||
});
|
||||
|
||||
if (!userHasPermission) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<Input>,
|
||||
featureFlagsMap: Record<FeatureFlagKey, boolean>,
|
||||
): Promise<Response>;
|
||||
|
||||
protected abstract validate(
|
||||
args: Input,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void>;
|
||||
}
|
||||
+542
@@ -0,0 +1,542 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
In,
|
||||
type FindOperator,
|
||||
type InsertResult,
|
||||
type ObjectLiteral,
|
||||
} from 'typeorm';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
|
||||
type PartialObjectRecordWithId = Partial<ObjectRecord> & { id: string };
|
||||
@Injectable()
|
||||
export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResolverService<
|
||||
CreateManyResolverArgs,
|
||||
ObjectRecord[]
|
||||
> {
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>,
|
||||
): Promise<ObjectRecord[]> {
|
||||
if (executionArgs.args.data.length > QUERY_MAX_RECORDS) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Maximum number of records to upsert is ${QUERY_MAX_RECORDS}.`,
|
||||
GraphqlQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED,
|
||||
{
|
||||
userFriendlyMessage: msg`Maximum number of records to upsert is ${QUERY_MAX_RECORDS}.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const { objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const objectRecords = await this.insertOrUpsertRecords(executionArgs);
|
||||
|
||||
const upsertedRecords = await this.fetchUpsertedRecords(
|
||||
executionArgs,
|
||||
objectRecords,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
|
||||
await this.processNestedRelationsIfNeeded({
|
||||
executionArgs,
|
||||
records: upsertedRecords,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
return this.formatRecordsForResponse(
|
||||
upsertedRecords,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
}
|
||||
|
||||
private async insertOrUpsertRecords(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>,
|
||||
): Promise<InsertResult> {
|
||||
if (!executionArgs.args.upsert) {
|
||||
const { objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const selectedColumns = buildColumnsToReturn({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
return await executionArgs.repository.insert(
|
||||
executionArgs.args.data,
|
||||
undefined,
|
||||
selectedColumns,
|
||||
);
|
||||
}
|
||||
|
||||
return this.performUpsertOperation(executionArgs);
|
||||
}
|
||||
|
||||
private async performUpsertOperation(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>,
|
||||
): Promise<InsertResult> {
|
||||
const { objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const conflictingFields = this.getConflictingFields(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
const existingRecords = await this.findExistingRecords(
|
||||
executionArgs,
|
||||
conflictingFields,
|
||||
);
|
||||
|
||||
const { recordsToUpdate, recordsToInsert } = this.categorizeRecords(
|
||||
executionArgs.args.data,
|
||||
conflictingFields,
|
||||
existingRecords,
|
||||
);
|
||||
|
||||
const result: InsertResult = {
|
||||
identifiers: [],
|
||||
generatedMaps: [],
|
||||
raw: [],
|
||||
};
|
||||
|
||||
const columnsToReturn = buildColumnsToReturn({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
if (recordsToUpdate.length > 0) {
|
||||
await this.processRecordsToUpdate({
|
||||
partialRecordsToUpdate: recordsToUpdate,
|
||||
repository: executionArgs.repository,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
result,
|
||||
columnsToReturn,
|
||||
});
|
||||
}
|
||||
|
||||
await this.processRecordsToInsert({
|
||||
recordsToInsert,
|
||||
repository: executionArgs.repository,
|
||||
result,
|
||||
columnsToReturn,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//TODO : Improve conflicting fields logic - unicity can be define on combination of fields - should be based on unique index not on field metadata
|
||||
//TODO : https://github.com/twentyhq/core-team-issues/issues/1115
|
||||
private getConflictingFields(
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
): {
|
||||
baseField: string;
|
||||
fullPath: string;
|
||||
column: string;
|
||||
}[] {
|
||||
return Object.values(objectMetadataItemWithFieldMaps.fieldsById)
|
||||
.filter((field) => field.isUnique || field.name === 'id')
|
||||
.flatMap((field) => {
|
||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||
|
||||
if (!compositeType) {
|
||||
return [
|
||||
{
|
||||
baseField: field.name,
|
||||
fullPath: field.name,
|
||||
column: field.name,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const property = compositeType.properties.find(
|
||||
(prop) => prop.isIncludedInUniqueConstraint,
|
||||
);
|
||||
|
||||
return property
|
||||
? [
|
||||
{
|
||||
baseField: field.name,
|
||||
fullPath: `${field.name}.${property.name}`,
|
||||
column: `${field.name}${capitalize(property.name)}`,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
});
|
||||
}
|
||||
|
||||
private async findExistingRecords(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>,
|
||||
conflictingFields: {
|
||||
baseField: string;
|
||||
fullPath: string;
|
||||
column: string;
|
||||
}[],
|
||||
): Promise<PartialObjectRecordWithId[]> {
|
||||
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
const whereConditions = this.buildWhereConditions(
|
||||
executionArgs.args.data,
|
||||
conflictingFields,
|
||||
);
|
||||
|
||||
whereConditions.forEach((condition) => {
|
||||
queryBuilder.orWhere(condition);
|
||||
});
|
||||
|
||||
const restrictedFields =
|
||||
executionArgs.repository.objectRecordsPermissions?.[
|
||||
objectMetadataItemWithFieldMaps.id
|
||||
]?.restrictedFields;
|
||||
|
||||
const selectOptions = getAllSelectableColumnNames({
|
||||
restrictedFields: restrictedFields ?? {},
|
||||
objectMetadata: {
|
||||
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
||||
},
|
||||
});
|
||||
|
||||
return (await queryBuilder
|
||||
.withDeleted()
|
||||
.setFindOptions({
|
||||
select: selectOptions,
|
||||
})
|
||||
.getMany()) as PartialObjectRecordWithId[];
|
||||
}
|
||||
|
||||
private getValueFromPath(
|
||||
record: Partial<ObjectRecord>,
|
||||
path: string,
|
||||
): unknown {
|
||||
const pathParts = path.split('.');
|
||||
|
||||
if (pathParts.length === 1) {
|
||||
return record[path];
|
||||
}
|
||||
|
||||
const [parentField, childField] = pathParts;
|
||||
|
||||
return record[parentField]?.[childField];
|
||||
}
|
||||
|
||||
private buildWhereConditions(
|
||||
records: Partial<ObjectRecord>[],
|
||||
conflictingFields: {
|
||||
baseField: string;
|
||||
fullPath: string;
|
||||
column: string;
|
||||
}[],
|
||||
): Record<string, FindOperator<string>>[] {
|
||||
const whereConditions = [];
|
||||
|
||||
for (const field of conflictingFields) {
|
||||
const fieldValues = records
|
||||
.map((record) => this.getValueFromPath(record, field.fullPath))
|
||||
.filter(Boolean);
|
||||
|
||||
//TODO : Adapt to composite constraint - https://github.com/twentyhq/core-team-issues/issues/1115
|
||||
if (fieldValues.length > 0) {
|
||||
whereConditions.push({ [field.column]: In(fieldValues) });
|
||||
}
|
||||
}
|
||||
|
||||
return whereConditions;
|
||||
}
|
||||
|
||||
private categorizeRecords(
|
||||
records: Partial<ObjectRecord>[],
|
||||
conflictingFields: {
|
||||
baseField: string;
|
||||
fullPath: string;
|
||||
column: string;
|
||||
}[],
|
||||
existingRecords: PartialObjectRecordWithId[],
|
||||
): {
|
||||
recordsToUpdate: PartialObjectRecordWithId[];
|
||||
recordsToInsert: Partial<ObjectRecord>[];
|
||||
} {
|
||||
const recordsToUpdate: PartialObjectRecordWithId[] = [];
|
||||
const recordsToInsert: Partial<ObjectRecord>[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
const matchingRecordId = this.getMatchingRecordId(
|
||||
record,
|
||||
conflictingFields,
|
||||
existingRecords,
|
||||
);
|
||||
|
||||
if (isDefined(matchingRecordId)) {
|
||||
recordsToUpdate.push({ ...record, id: matchingRecordId });
|
||||
} else {
|
||||
recordsToInsert.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
return { recordsToUpdate, recordsToInsert };
|
||||
}
|
||||
|
||||
private getMatchingRecordId(
|
||||
record: Partial<ObjectRecord>,
|
||||
conflictingFields: {
|
||||
baseField: string;
|
||||
fullPath: string;
|
||||
column: string;
|
||||
}[],
|
||||
existingRecords: PartialObjectRecordWithId[],
|
||||
): string | undefined {
|
||||
const matchingRecordIds = conflictingFields.reduce<string[]>(
|
||||
(acc, field) => {
|
||||
const requestFieldValue = this.getValueFromPath(record, field.fullPath);
|
||||
|
||||
const matchingRecord = existingRecords.find((existingRecord) => {
|
||||
const existingFieldValue = this.getValueFromPath(
|
||||
existingRecord,
|
||||
field.fullPath,
|
||||
);
|
||||
|
||||
return (
|
||||
isDefined(existingFieldValue) &&
|
||||
existingFieldValue === requestFieldValue
|
||||
);
|
||||
});
|
||||
|
||||
if (isDefined(matchingRecord)) {
|
||||
acc.push(matchingRecord.id);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if ([...new Set(matchingRecordIds)].length > 1) {
|
||||
const conflictingFieldsValues = conflictingFields
|
||||
.map((field) => {
|
||||
const value = this.getValueFromPath(record, field.fullPath);
|
||||
|
||||
return isDefined(value) ? `${field.fullPath}: ${value}` : undefined;
|
||||
})
|
||||
.filter(isDefined)
|
||||
.join(', ');
|
||||
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Multiple records found with the same unique field values for ${conflictingFieldsValues}. Cannot determine which record to update.`,
|
||||
GraphqlQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT,
|
||||
{
|
||||
userFriendlyMessage: msg`Multiple records found with the same unique field values for ${conflictingFieldsValues}. Cannot determine which record to update.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return matchingRecordIds[0];
|
||||
}
|
||||
|
||||
private async processRecordsToUpdate({
|
||||
partialRecordsToUpdate,
|
||||
repository,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
result,
|
||||
columnsToReturn,
|
||||
}: {
|
||||
partialRecordsToUpdate: PartialObjectRecordWithId[];
|
||||
repository: WorkspaceRepository<ObjectLiteral>;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
result: InsertResult;
|
||||
columnsToReturn: string[];
|
||||
}): Promise<void> {
|
||||
const partialRecordsToUpdateWithoutCreatedByUpdate =
|
||||
partialRecordsToUpdate.map((record) =>
|
||||
this.getRecordWithoutCreatedBy(record, objectMetadataItemWithFieldMaps),
|
||||
);
|
||||
|
||||
const savedRecords = await repository.updateMany(
|
||||
partialRecordsToUpdateWithoutCreatedByUpdate.map((record) => ({
|
||||
criteria: record.id,
|
||||
partialEntity: { ...record, deletedAt: null },
|
||||
})),
|
||||
undefined,
|
||||
columnsToReturn,
|
||||
);
|
||||
|
||||
result.identifiers.push(
|
||||
...savedRecords.generatedMaps.map((record) => ({ id: record.id })),
|
||||
);
|
||||
result.generatedMaps.push(
|
||||
...savedRecords.generatedMaps.map((record) => ({ id: record.id })),
|
||||
);
|
||||
}
|
||||
|
||||
private async processRecordsToInsert({
|
||||
recordsToInsert,
|
||||
repository,
|
||||
result,
|
||||
columnsToReturn,
|
||||
}: {
|
||||
recordsToInsert: Partial<ObjectRecord>[];
|
||||
repository: WorkspaceRepository<ObjectLiteral>;
|
||||
result: InsertResult;
|
||||
columnsToReturn: string[];
|
||||
}): Promise<void> {
|
||||
if (recordsToInsert.length > 0) {
|
||||
const insertResult = await repository.insert(
|
||||
recordsToInsert,
|
||||
undefined,
|
||||
columnsToReturn,
|
||||
);
|
||||
|
||||
result.identifiers.push(...insertResult.identifiers);
|
||||
result.generatedMaps.push(...insertResult.generatedMaps);
|
||||
result.raw.push(...insertResult.raw);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchUpsertedRecords(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>,
|
||||
objectRecords: InsertResult,
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
): Promise<ObjectRecord[]> {
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
const columnsToSelect = buildColumnsToSelect({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const upsertedRecords = await queryBuilder
|
||||
.setFindOptions({
|
||||
select: columnsToSelect,
|
||||
})
|
||||
.where({
|
||||
id: In(objectRecords.generatedMaps.map((record) => record.id)),
|
||||
})
|
||||
.withDeleted()
|
||||
.take(QUERY_MAX_RECORDS)
|
||||
.getMany();
|
||||
|
||||
return upsertedRecords as ObjectRecord[];
|
||||
}
|
||||
|
||||
private async processNestedRelationsIfNeeded({
|
||||
executionArgs,
|
||||
records,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>;
|
||||
records: ObjectRecord[];
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
}): Promise<void> {
|
||||
if (!executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords: records,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext: executionArgs.options.authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
private formatRecordsForResponse(
|
||||
upsertedRecords: ObjectRecord[],
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
): ObjectRecord[] {
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return upsertedRecords.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getRecordWithoutCreatedBy(
|
||||
record: PartialObjectRecordWithId,
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
): Omit<PartialObjectRecordWithId, 'createdBy'> {
|
||||
let recordWithoutCreatedByUpdate = record;
|
||||
|
||||
const createdByFieldMetadataId =
|
||||
objectMetadataItemWithFieldMaps.fieldIdByName['createdBy'];
|
||||
const createdByFieldMetadata =
|
||||
objectMetadataItemWithFieldMaps.fieldsById[createdByFieldMetadataId];
|
||||
|
||||
if (!isDefined(createdByFieldMetadata)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Missing createdBy field metadata for object ${objectMetadataItemWithFieldMaps.nameSingular}`,
|
||||
GraphqlQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD,
|
||||
);
|
||||
}
|
||||
|
||||
if ('createdBy' in record && createdByFieldMetadata.isCustom === false) {
|
||||
const { createdBy: _createdBy, ...recordWithoutCreatedBy } = record;
|
||||
|
||||
recordWithoutCreatedByUpdate = recordWithoutCreatedBy;
|
||||
}
|
||||
|
||||
return recordWithoutCreatedByUpdate;
|
||||
}
|
||||
|
||||
async validate<T extends ObjectRecord>(
|
||||
args: CreateManyResolverArgs<Partial<T>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
|
||||
|
||||
args.data.forEach((record) => {
|
||||
if (record?.id) {
|
||||
assertIsValidUuid(record.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolverService<
|
||||
CreateOneResolverArgs,
|
||||
ObjectRecord
|
||||
> {
|
||||
constructor(
|
||||
private readonly createManyResolverService: GraphqlQueryCreateManyResolverService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<CreateOneResolverArgs>,
|
||||
): Promise<ObjectRecord> {
|
||||
const result = await this.createManyResolverService.resolve({
|
||||
...executionArgs,
|
||||
args: {
|
||||
...executionArgs.args,
|
||||
data: [executionArgs.args.data],
|
||||
},
|
||||
});
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: CreateOneResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
|
||||
|
||||
if (args.data?.id) {
|
||||
assertIsValidUuid(args.data.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type DeleteManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResolverService<
|
||||
DeleteManyResolverArgs,
|
||||
ObjectRecord[]
|
||||
> {
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<DeleteManyResolverArgs>,
|
||||
): Promise<ObjectRecord[]> {
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
executionArgs.graphqlQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
executionArgs.args.filter,
|
||||
);
|
||||
|
||||
const columnsToReturn = buildColumnsToReturn({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const deletedObjectRecords = await queryBuilder
|
||||
.softDelete()
|
||||
.returning(columnsToReturn)
|
||||
.execute();
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords:
|
||||
deletedObjectRecords.generatedMaps as ObjectRecord[],
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return deletedObjectRecords.generatedMaps.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: DeleteManyResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
|
||||
if (!args.filter) {
|
||||
throw new Error('Filter is required');
|
||||
}
|
||||
|
||||
args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id));
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolverService<
|
||||
DeleteOneResolverArgs,
|
||||
ObjectRecord
|
||||
> {
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<DeleteOneResolverArgs>,
|
||||
): Promise<ObjectRecord> {
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
const columnsToReturn = buildColumnsToReturn({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const deletedObjectRecords = await queryBuilder
|
||||
.softDelete()
|
||||
.where({ id: executionArgs.args.id })
|
||||
.returning(columnsToReturn)
|
||||
.execute();
|
||||
|
||||
if (deletedObjectRecords.generatedMaps.length === 0) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Record not found',
|
||||
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const deletedRecord = deletedObjectRecords.generatedMaps[0] as ObjectRecord;
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords: [deletedRecord],
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
const result = typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: deletedRecord,
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: DeleteOneResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
|
||||
assertIsValidUuid(args.id);
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseResolverService<
|
||||
DestroyManyResolverArgs,
|
||||
ObjectRecord[]
|
||||
> {
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<DestroyManyResolverArgs>,
|
||||
): Promise<ObjectRecord[]> {
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
executionArgs.graphqlQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
executionArgs.args.filter,
|
||||
);
|
||||
|
||||
const columnsToReturn = buildColumnsToReturn({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const deletedObjectRecords = await queryBuilder
|
||||
.delete()
|
||||
.returning(columnsToReturn)
|
||||
.execute();
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords:
|
||||
deletedObjectRecords.generatedMaps as ObjectRecord[],
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return deletedObjectRecords.generatedMaps.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: DestroyManyResolverArgs,
|
||||
_options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
if (!args.filter) {
|
||||
throw new Error('Filter is required');
|
||||
}
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type DestroyOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResolverService<
|
||||
DestroyOneResolverArgs,
|
||||
ObjectRecord
|
||||
> {
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<DestroyOneResolverArgs>,
|
||||
): Promise<ObjectRecord> {
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
const columnsToReturn = buildColumnsToReturn({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const deletedObjectRecords = await queryBuilder
|
||||
.delete()
|
||||
.where({ id: executionArgs.args.id })
|
||||
.returning(columnsToReturn)
|
||||
.execute();
|
||||
|
||||
if (!deletedObjectRecords.affected) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Record not found',
|
||||
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords:
|
||||
deletedObjectRecords.generatedMaps as ObjectRecord[],
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: deletedObjectRecords.generatedMaps[0] as ObjectRecord,
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: DestroyOneResolverArgs,
|
||||
_options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
if (!args.id) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Missing id',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { OrderByDirection, type ObjectRecord } from 'twenty-shared/types';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type FindDuplicatesResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
|
||||
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseResolverService<
|
||||
FindDuplicatesResolverArgs,
|
||||
IConnection<ObjectRecord>[]
|
||||
> {
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<FindDuplicatesResolverArgs>,
|
||||
): Promise<IConnection<ObjectRecord>[]> {
|
||||
const { objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const existingRecordsQueryBuilder =
|
||||
executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
const objectMetadataItemWithFieldsMaps =
|
||||
getObjectMetadataMapItemByNameSingular(
|
||||
objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
if (!objectMetadataItemWithFieldsMaps) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Object ${objectMetadataItemWithFieldMaps.nameSingular} not found`,
|
||||
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
let objectRecords: Partial<ObjectRecord>[] = [];
|
||||
|
||||
const columnsToSelect = buildColumnsToSelect({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
if (executionArgs.args.ids) {
|
||||
objectRecords = (await existingRecordsQueryBuilder
|
||||
.where({ id: In(executionArgs.args.ids) })
|
||||
.setFindOptions({
|
||||
select: columnsToSelect,
|
||||
})
|
||||
.getMany()) as ObjectRecord[];
|
||||
} else if (executionArgs.args.data && !isEmpty(executionArgs.args.data)) {
|
||||
objectRecords = executionArgs.args.data;
|
||||
}
|
||||
|
||||
const duplicateConnections: IConnection<ObjectRecord>[] = await Promise.all(
|
||||
objectRecords.map(async (record) => {
|
||||
const duplicateConditions = buildDuplicateConditions(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
[record],
|
||||
record.id,
|
||||
);
|
||||
|
||||
if (isEmpty(duplicateConditions)) {
|
||||
return typeORMObjectRecordsParser.createConnection({
|
||||
objectRecords: [],
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 0,
|
||||
totalCount: 0,
|
||||
order: [{ id: OrderByDirection.AscNullsFirst }],
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
});
|
||||
}
|
||||
|
||||
const duplicateRecordsQueryBuilder =
|
||||
executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
graphqlQueryParser.applyFilterToBuilder(
|
||||
duplicateRecordsQueryBuilder,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
duplicateConditions,
|
||||
);
|
||||
|
||||
const duplicates = (await duplicateRecordsQueryBuilder
|
||||
.setFindOptions({
|
||||
select: columnsToSelect,
|
||||
})
|
||||
.take(QUERY_MAX_RECORDS)
|
||||
.getMany()) as ObjectRecord[];
|
||||
|
||||
return typeORMObjectRecordsParser.createConnection({
|
||||
objectRecords: duplicates,
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: duplicates.length,
|
||||
totalCount: duplicates.length,
|
||||
order: [{ id: OrderByDirection.AscNullsFirst }],
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return duplicateConnections;
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: FindDuplicatesResolverArgs,
|
||||
_options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
if (!args.data && !args.ids) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'You have to provide either "data" or "ids" argument',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (args.data && args.ids) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'You cannot provide both "data" and "ids" arguments',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (!args.ids && isEmpty(args.data)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'The "data" condition can not be empty when "ids" input not provided',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { ObjectRecord, OrderByDirection } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import {
|
||||
type ObjectRecordFilter,
|
||||
type ObjectRecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
|
||||
import {
|
||||
getCursor,
|
||||
getPaginationInfo,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
|
||||
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolverService<
|
||||
FindManyResolverArgs,
|
||||
IConnection<ObjectRecord>
|
||||
> {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<FindManyResolverArgs>,
|
||||
): Promise<IConnection<ObjectRecord>> {
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const objectMetadataNameSingular =
|
||||
objectMetadataItemWithFieldMaps.nameSingular;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataNameSingular,
|
||||
);
|
||||
|
||||
const aggregateQueryBuilder = queryBuilder.clone();
|
||||
|
||||
let appliedFilters =
|
||||
executionArgs.args.filter ?? ({} as ObjectRecordFilter);
|
||||
|
||||
executionArgs.graphqlQueryParser.applyFilterToBuilder(
|
||||
aggregateQueryBuilder,
|
||||
objectMetadataNameSingular,
|
||||
appliedFilters,
|
||||
);
|
||||
|
||||
executionArgs.graphqlQueryParser.applyDeletedAtToBuilder(
|
||||
aggregateQueryBuilder,
|
||||
appliedFilters,
|
||||
);
|
||||
|
||||
const orderByWithIdCondition = [
|
||||
...(executionArgs.args.orderBy ?? []),
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
] as ObjectRecordOrderBy;
|
||||
|
||||
const isForwardPagination = !isDefined(executionArgs.args.before);
|
||||
|
||||
const cursor = getCursor(executionArgs.args);
|
||||
|
||||
if (cursor) {
|
||||
const cursorArgFilter = computeCursorArgFilter(
|
||||
cursor,
|
||||
orderByWithIdCondition,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
appliedFilters = (executionArgs.args.filter
|
||||
? {
|
||||
and: [executionArgs.args.filter, { or: cursorArgFilter }],
|
||||
}
|
||||
: { or: cursorArgFilter }) as unknown as ObjectRecordFilter;
|
||||
}
|
||||
|
||||
executionArgs.graphqlQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataNameSingular,
|
||||
appliedFilters,
|
||||
);
|
||||
|
||||
executionArgs.graphqlQueryParser.applyOrderToBuilder(
|
||||
queryBuilder,
|
||||
orderByWithIdCondition,
|
||||
objectMetadataNameSingular,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
executionArgs.graphqlQueryParser.applyDeletedAtToBuilder(
|
||||
queryBuilder,
|
||||
appliedFilters,
|
||||
);
|
||||
|
||||
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
|
||||
selectedAggregatedFields:
|
||||
executionArgs.graphqlQuerySelectedFieldsResult.aggregate,
|
||||
queryBuilder: aggregateQueryBuilder,
|
||||
objectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const limit =
|
||||
executionArgs.args.first ?? executionArgs.args.last ?? QUERY_MAX_RECORDS;
|
||||
|
||||
const columnsToSelect = buildColumnsToSelect({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
if (isDefined(executionArgs.args.offset)) {
|
||||
queryBuilder.skip(executionArgs.args.offset);
|
||||
}
|
||||
|
||||
const objectRecords = (await queryBuilder
|
||||
.setFindOptions({
|
||||
select: columnsToSelect,
|
||||
})
|
||||
.take(limit + 1)
|
||||
.getMany()) as ObjectRecord[];
|
||||
|
||||
const { hasNextPage, hasPreviousPage } = getPaginationInfo(
|
||||
objectRecords,
|
||||
limit,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
if (objectRecords.length > limit) {
|
||||
objectRecords.pop();
|
||||
}
|
||||
|
||||
const parentObjectRecordsAggregatedValues =
|
||||
await aggregateQueryBuilder.getRawOne();
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords: objectRecords,
|
||||
parentObjectRecordsAggregatedValues,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
aggregate: executionArgs.graphqlQuerySelectedFieldsResult.aggregate,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return typeORMObjectRecordsParser.createConnection({
|
||||
objectRecords: isForwardPagination
|
||||
? objectRecords
|
||||
: objectRecords.reverse(),
|
||||
objectRecordsAggregatedValues: parentObjectRecordsAggregatedValues,
|
||||
selectedAggregatedFields:
|
||||
executionArgs.graphqlQuerySelectedFieldsResult.aggregate,
|
||||
objectName: objectMetadataNameSingular,
|
||||
take: limit,
|
||||
totalCount: parentObjectRecordsAggregatedValues?.totalCount,
|
||||
order: orderByWithIdCondition,
|
||||
hasNextPage,
|
||||
hasPreviousPage,
|
||||
});
|
||||
}
|
||||
|
||||
async validate<Filter extends ObjectRecordFilter>(
|
||||
args: FindManyResolverArgs<Filter>,
|
||||
_options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
if (args.first && args.last) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Cannot provide both first and last',
|
||||
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
|
||||
);
|
||||
}
|
||||
if (args.before && args.after) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Cannot provide both before and after',
|
||||
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
|
||||
);
|
||||
}
|
||||
if (args.before && args.first) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Cannot provide both before and first',
|
||||
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
|
||||
);
|
||||
}
|
||||
if (args.after && args.last) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Cannot provide both after and last',
|
||||
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
|
||||
);
|
||||
}
|
||||
if (args.first !== undefined && args.first < 0) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'First argument must be non-negative',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_ARGS_FIRST,
|
||||
);
|
||||
}
|
||||
if (args.last !== undefined && args.last < 0) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Last argument must be non-negative',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_ARGS_LAST,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
//TODO : Refacto-common - To delete
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
|
||||
import {
|
||||
WorkspaceQueryRunnerException,
|
||||
WorkspaceQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolverService<
|
||||
FindOneResolverArgs,
|
||||
ObjectRecord
|
||||
> {
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<FindOneResolverArgs>,
|
||||
): Promise<ObjectRecord> {
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
executionArgs.graphqlQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
executionArgs.args.filter ?? ({} as ObjectRecordFilter),
|
||||
);
|
||||
|
||||
executionArgs.graphqlQueryParser.applyDeletedAtToBuilder(
|
||||
queryBuilder,
|
||||
executionArgs.args.filter ?? ({} as ObjectRecordFilter),
|
||||
);
|
||||
|
||||
const columnsToSelect = buildColumnsToSelect({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const objectRecord = await queryBuilder
|
||||
.setFindOptions({
|
||||
select: columnsToSelect,
|
||||
})
|
||||
.getOne();
|
||||
|
||||
if (!objectRecord) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Record not found',
|
||||
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const objectRecords = [objectRecord] as ObjectRecord[];
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords: objectRecords,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: objectRecords[0],
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}) as ObjectRecord;
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: FindOneResolverArgs<ObjectRecordFilter>,
|
||||
_options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
if (!args.filter || Object.keys(args.filter).length === 0) {
|
||||
throw new WorkspaceQueryRunnerException(
|
||||
'Missing filter argument',
|
||||
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
MUTATION_MAX_MERGE_RECORDS,
|
||||
QUERY_MAX_RECORDS,
|
||||
} from 'twenty-shared/constants';
|
||||
import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In } from 'typeorm';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type MergeManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { type FieldMetadataRelationSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||
import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util';
|
||||
import { mergeFieldValues } from 'src/engine/api/graphql/graphql-query-runner/utils/merge-field-values.util';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolverService<
|
||||
MergeManyResolverArgs,
|
||||
ObjectRecord
|
||||
> {
|
||||
private readonly logger = new Logger(
|
||||
GraphqlQueryMergeManyResolverService.name,
|
||||
);
|
||||
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<MergeManyResolverArgs>,
|
||||
): Promise<ObjectRecord> {
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
const { ids, conflictPriorityIndex, dryRun } = executionArgs.args;
|
||||
|
||||
const recordsToMerge = await this.fetchRecordsToMerge(executionArgs, ids);
|
||||
|
||||
const priorityRecord = this.validateAndGetPriorityRecord(
|
||||
recordsToMerge,
|
||||
ids,
|
||||
conflictPriorityIndex,
|
||||
);
|
||||
|
||||
const mergedData = this.performDeepMerge(
|
||||
recordsToMerge,
|
||||
priorityRecord.id,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
return this.createDryRunResponse(
|
||||
priorityRecord,
|
||||
mergedData,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
}
|
||||
|
||||
const idsToDelete = ids.filter((id) => id !== priorityRecord.id);
|
||||
|
||||
await this.migrateRelatedRecords(
|
||||
executionArgs,
|
||||
idsToDelete,
|
||||
priorityRecord.id,
|
||||
);
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
await queryBuilder
|
||||
.softDelete()
|
||||
.whereInIds(idsToDelete)
|
||||
.returning('*')
|
||||
.execute();
|
||||
|
||||
const updatedRecord = await this.updatePriorityRecord(
|
||||
executionArgs,
|
||||
priorityRecord.id,
|
||||
mergedData,
|
||||
);
|
||||
|
||||
if (executionArgs.rolePermissionConfig) {
|
||||
await this.processNestedRelations({
|
||||
executionArgs,
|
||||
updatedRecords: [updatedRecord],
|
||||
authContext,
|
||||
});
|
||||
}
|
||||
|
||||
return this.formatResponse(
|
||||
updatedRecord,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
}
|
||||
|
||||
private async fetchRecordsToMerge(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<MergeManyResolverArgs>,
|
||||
ids: string[],
|
||||
): Promise<ObjectRecord[]> {
|
||||
const recordsToMerge = await executionArgs.repository.find({
|
||||
where: { id: In(ids) },
|
||||
});
|
||||
|
||||
if (recordsToMerge.length !== ids.length) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'One or more records not found',
|
||||
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return recordsToMerge as ObjectRecord[];
|
||||
}
|
||||
|
||||
private validateAndGetPriorityRecord(
|
||||
recordsToMerge: ObjectRecord[],
|
||||
ids: string[],
|
||||
conflictPriorityIndex: number,
|
||||
): ObjectRecord {
|
||||
const priorityRecordId = ids[conflictPriorityIndex];
|
||||
const priorityRecord = recordsToMerge.find(
|
||||
(record) => record.id === priorityRecordId,
|
||||
);
|
||||
|
||||
if (!priorityRecord) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Priority record not found',
|
||||
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return priorityRecord;
|
||||
}
|
||||
|
||||
private performDeepMerge(
|
||||
recordsToMerge: ObjectRecord[],
|
||||
priorityRecordId: string,
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
): Partial<ObjectRecord> {
|
||||
const mergedResult: Partial<ObjectRecord> = {};
|
||||
|
||||
const allFieldNames = new Set<string>();
|
||||
|
||||
recordsToMerge.forEach((record) => {
|
||||
Object.keys(record).forEach((fieldName) => {
|
||||
if (
|
||||
!this.shouldExcludeFieldFromMerge(
|
||||
fieldName,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
)
|
||||
) {
|
||||
allFieldNames.add(fieldName);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
allFieldNames.forEach((fieldName) => {
|
||||
const recordsWithValues: { value: unknown; recordId: string }[] = [];
|
||||
|
||||
recordsToMerge.forEach((record) => {
|
||||
const fieldValue = record[fieldName];
|
||||
|
||||
if (hasRecordFieldValue(fieldValue)) {
|
||||
recordsWithValues.push({ value: fieldValue, recordId: record.id });
|
||||
}
|
||||
});
|
||||
|
||||
if (recordsWithValues.length === 0) {
|
||||
return;
|
||||
} else if (recordsWithValues.length === 1) {
|
||||
mergedResult[fieldName] = recordsWithValues[0].value;
|
||||
} else {
|
||||
const fieldMetadata = Object.values(
|
||||
objectMetadataItemWithFieldMaps.fieldsById,
|
||||
).find((field) => field?.name === fieldName);
|
||||
|
||||
if (!fieldMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
mergedResult[fieldName] = mergeFieldValues(
|
||||
fieldMetadata.type,
|
||||
recordsWithValues,
|
||||
priorityRecordId,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return mergedResult;
|
||||
}
|
||||
|
||||
private shouldExcludeFieldFromMerge(
|
||||
fieldName: string,
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
): boolean {
|
||||
const fieldMetadata = Object.values(
|
||||
objectMetadataItemWithFieldMaps.fieldsById,
|
||||
).find((field) => field?.name === fieldName);
|
||||
|
||||
return fieldMetadata?.isSystem ?? false;
|
||||
}
|
||||
|
||||
private createDryRunResponse(
|
||||
priorityRecord: ObjectRecord,
|
||||
mergedData: Partial<ObjectRecord>,
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
): ObjectRecord {
|
||||
const dryRunRecord = {
|
||||
...priorityRecord,
|
||||
...mergedData,
|
||||
id: uuidv4(),
|
||||
deletedAt: new Date().toISOString(),
|
||||
} as ObjectRecord;
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: dryRunRecord,
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
}
|
||||
|
||||
private async updatePriorityRecord(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<MergeManyResolverArgs>,
|
||||
priorityRecordId: string,
|
||||
mergedData: Partial<ObjectRecord>,
|
||||
): Promise<ObjectRecord> {
|
||||
const { objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
const columnsToReturn = buildColumnsToReturn({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const updatedObjectRecords = await queryBuilder
|
||||
.update()
|
||||
.set(mergedData)
|
||||
.where({ id: priorityRecordId })
|
||||
.returning(columnsToReturn)
|
||||
.execute();
|
||||
|
||||
if (!updatedObjectRecords.generatedMaps.length) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Failed to update record',
|
||||
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const updatedRecord = updatedObjectRecords.generatedMaps[0] as ObjectRecord;
|
||||
|
||||
return updatedRecord;
|
||||
}
|
||||
|
||||
private async migrateRelatedRecords(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<MergeManyResolverArgs>,
|
||||
fromIds: string[],
|
||||
toId: string,
|
||||
): Promise<void> {
|
||||
const { objectMetadataMaps, objectMetadataItemWithFieldMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const relationFieldsPointingToCurrentObject = Object.values(
|
||||
objectMetadataMaps.byId,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.flatMap((metadata) => {
|
||||
const relationFields = Object.values(metadata.fieldsById)
|
||||
.filter(isDefined)
|
||||
.filter((field) =>
|
||||
isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION),
|
||||
)
|
||||
.filter(
|
||||
(field) =>
|
||||
field.relationTargetObjectMetadataId ===
|
||||
objectMetadataItemWithFieldMaps.id && field.isActive,
|
||||
);
|
||||
|
||||
return relationFields
|
||||
.filter((field) => {
|
||||
const relationSettings =
|
||||
field.settings as FieldMetadataRelationSettings;
|
||||
|
||||
return (
|
||||
relationSettings?.relationType === RelationType.MANY_TO_ONE &&
|
||||
relationSettings?.joinColumnName
|
||||
);
|
||||
})
|
||||
.map((field) => {
|
||||
const relationSettings =
|
||||
field.settings as FieldMetadataRelationSettings;
|
||||
|
||||
return {
|
||||
objectMetadata: metadata,
|
||||
fieldName: field.name,
|
||||
fieldId: field.id,
|
||||
joinColumnName: relationSettings.joinColumnName,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
for (const relationField of relationFieldsPointingToCurrentObject) {
|
||||
if (!relationField.joinColumnName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const repository = executionArgs.workspaceDataSource.getRepository(
|
||||
relationField.objectMetadata.nameSingular,
|
||||
executionArgs.rolePermissionConfig,
|
||||
);
|
||||
|
||||
const whereCondition = { [relationField.joinColumnName]: In(fromIds) };
|
||||
|
||||
const existingRecords = await repository.find({
|
||||
where: whereCondition,
|
||||
});
|
||||
|
||||
if (existingRecords.length > 0) {
|
||||
await repository.update(whereCondition, {
|
||||
[relationField.joinColumnName]: toId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to migrate relation field "${relationField.fieldName}" (${relationField.joinColumnName}) in object "${relationField.objectMetadata.nameSingular}":`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async processNestedRelations({
|
||||
executionArgs,
|
||||
updatedRecords,
|
||||
authContext,
|
||||
}: {
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<MergeManyResolverArgs>;
|
||||
updatedRecords: ObjectRecord[];
|
||||
authContext: AuthContext;
|
||||
}): Promise<void> {
|
||||
const { objectMetadataMaps, objectMetadataItemWithFieldMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords: updatedRecords,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private formatResponse(
|
||||
updatedRecord: ObjectRecord,
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
): ObjectRecord {
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: updatedRecord,
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: MergeManyResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
|
||||
|
||||
if (!isDefined(options.objectMetadataItemWithFieldMaps.duplicateCriteria)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Merge is only available for objects with duplicate criteria. Object '${options.objectMetadataItemWithFieldMaps.nameSingular}' does not have duplicate criteria defined.`,
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const { ids, conflictPriorityIndex } = args;
|
||||
|
||||
if (!ids || ids.length < 2) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'At least 2 record IDs are required for merge',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (ids.length > MUTATION_MAX_MERGE_RECORDS) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Maximum ${MUTATION_MAX_MERGE_RECORDS} records can be merged at once`,
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (conflictPriorityIndex < 0 || conflictPriorityIndex >= ids.length) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Invalid conflict priority '${conflictPriorityIndex}'. Valid options for ${ids.length} records: 0-${ids.length - 1}`,
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type RestoreManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseResolverService<
|
||||
RestoreManyResolverArgs,
|
||||
ObjectRecord[]
|
||||
> {
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<RestoreManyResolverArgs>,
|
||||
): Promise<ObjectRecord[]> {
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
executionArgs.graphqlQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
executionArgs.args.filter,
|
||||
);
|
||||
|
||||
const columnsToReturn = buildColumnsToReturn({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const restoredObjectRecords = await queryBuilder
|
||||
.restore()
|
||||
.returning(columnsToReturn)
|
||||
.execute();
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords:
|
||||
restoredObjectRecords.generatedMaps as ObjectRecord[],
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return restoredObjectRecords.generatedMaps.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: RestoreManyResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
|
||||
if (!args.filter) {
|
||||
throw new Error('Filter is required');
|
||||
}
|
||||
|
||||
args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id));
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type RestoreOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResolverService<
|
||||
RestoreOneResolverArgs,
|
||||
ObjectRecord
|
||||
> {
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<RestoreOneResolverArgs>,
|
||||
): Promise<ObjectRecord> {
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
const columnsToReturn = buildColumnsToReturn({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const restoredObjectRecords = await queryBuilder
|
||||
.restore()
|
||||
.where({ id: executionArgs.args.id })
|
||||
.returning(columnsToReturn)
|
||||
.execute();
|
||||
|
||||
if (restoredObjectRecords.generatedMaps.length === 0) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Record not found',
|
||||
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const restoredRecord = restoredObjectRecords
|
||||
.generatedMaps[0] as ObjectRecord;
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords: [restoredRecord],
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: restoredRecord,
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: RestoreOneResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
|
||||
assertIsValidUuid(args.id);
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResolverService<
|
||||
UpdateManyResolverArgs,
|
||||
ObjectRecord[]
|
||||
> {
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<UpdateManyResolverArgs>,
|
||||
): Promise<ObjectRecord[]> {
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
executionArgs.graphqlQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
executionArgs.args.filter,
|
||||
);
|
||||
|
||||
const columnsToReturn = buildColumnsToReturn({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const updatedObjectRecords = await queryBuilder
|
||||
.update()
|
||||
.set(executionArgs.args.data)
|
||||
.returning(columnsToReturn)
|
||||
.execute();
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords:
|
||||
updatedObjectRecords.generatedMaps as ObjectRecord[],
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return updatedObjectRecords.generatedMaps.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: UpdateManyResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
|
||||
if (!args.filter) {
|
||||
throw new Error('Filter is required');
|
||||
}
|
||||
|
||||
args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id));
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
type GraphqlQueryResolverExecutionArgs,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolverService<
|
||||
UpdateOneResolverArgs,
|
||||
ObjectRecord
|
||||
> {
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<UpdateOneResolverArgs>,
|
||||
): Promise<ObjectRecord> {
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
|
||||
const columnsToReturn = buildColumnsToReturn({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const updatedObjectRecords = await queryBuilder
|
||||
.update()
|
||||
.set(executionArgs.args.data)
|
||||
.where({ id: executionArgs.args.id })
|
||||
.returning(columnsToReturn)
|
||||
.execute();
|
||||
|
||||
const updatedRecord = updatedObjectRecords.generatedMaps[0] as ObjectRecord;
|
||||
|
||||
if (!updatedRecord) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Record not found',
|
||||
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
parentObjectRecords: [updatedRecord],
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: updatedRecord,
|
||||
objectName: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: UpdateOneResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
|
||||
assertIsValidUuid(args.id);
|
||||
}
|
||||
}
|
||||
+49
-20
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type CreateManyResolverArgs,
|
||||
@@ -12,8 +13,11 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works
|
||||
|
||||
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class CreateManyResolverFactory
|
||||
@@ -22,7 +26,9 @@ export class CreateManyResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.CREATE_MANY;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryCreateManyResolverService,
|
||||
private readonly commonCreateManyQueryRunnerService: CommonCreateManyQueryRunnerService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
create(
|
||||
@@ -31,31 +37,54 @@ export class CreateManyResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: internalContext.authContext.workspace?.id as string,
|
||||
});
|
||||
|
||||
try {
|
||||
const records = await this.commonCreateManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
try {
|
||||
const records = await this.commonCreateManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
return records.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return records.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
CreateManyResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+45
-18
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import graphqlFields from 'graphql-fields';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type CreateOneResolverArgs,
|
||||
@@ -11,8 +12,11 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works
|
||||
|
||||
import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryCreateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
|
||||
@Injectable()
|
||||
export class CreateOneResolverFactory
|
||||
@@ -21,6 +25,8 @@ export class CreateOneResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.CREATE_ONE;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryCreateOneResolverService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly commonCreateOneQueryRunnerService: CommonCreateOneQueryRunnerService,
|
||||
) {}
|
||||
|
||||
@@ -30,29 +36,50 @@ export class CreateOneResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
internalContext.authContext.workspace?.id as string,
|
||||
);
|
||||
|
||||
try {
|
||||
const record = await this.commonCreateOneQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
if (isCommonApiEnabled) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
try {
|
||||
const record = await this.commonCreateOneQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
CreateOneResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+49
-20
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type DeleteManyResolverArgs,
|
||||
@@ -12,8 +13,11 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works
|
||||
|
||||
import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryDeleteManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteManyResolverFactory
|
||||
@@ -22,7 +26,9 @@ export class DeleteManyResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.DELETE_MANY;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryDeleteManyResolverService,
|
||||
private readonly commonDeleteManyQueryRunnerService: CommonDeleteManyQueryRunnerService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
create(
|
||||
@@ -31,31 +37,54 @@ export class DeleteManyResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: internalContext.authContext.workspace?.id as string,
|
||||
});
|
||||
|
||||
try {
|
||||
const records = await this.commonDeleteManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
try {
|
||||
const records = await this.commonDeleteManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
return records.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return records.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
DeleteManyResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+47
-18
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import graphqlFields from 'graphql-fields';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type DeleteOneResolverArgs,
|
||||
@@ -11,8 +12,11 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works
|
||||
|
||||
import { CommonDeleteOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-one-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryDeleteOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteOneResolverFactory
|
||||
@@ -21,7 +25,9 @@ export class DeleteOneResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.DELETE_ONE;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryDeleteOneResolverService,
|
||||
private readonly commonDeleteOneQueryRunnerService: CommonDeleteOneQueryRunnerService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
create(
|
||||
@@ -30,29 +36,52 @@ export class DeleteOneResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: internalContext.authContext.workspace?.id as string,
|
||||
});
|
||||
|
||||
try {
|
||||
const record = await this.commonDeleteOneQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
try {
|
||||
const record = await this.commonDeleteOneQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
DeleteOneResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+51
-21
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type DestroyManyResolverArgs,
|
||||
@@ -12,8 +13,11 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works
|
||||
|
||||
import { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryDestroyManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class DestroyManyResolverFactory
|
||||
@@ -22,7 +26,9 @@ export class DestroyManyResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.DESTROY_MANY;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryDestroyManyResolverService,
|
||||
private readonly commonDestroyManyQueryRunnerService: CommonDestroyManyQueryRunnerService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
create(
|
||||
@@ -31,31 +37,55 @@ export class DestroyManyResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: internalContext.authContext.workspace?.id as string,
|
||||
});
|
||||
|
||||
try {
|
||||
const records = await this.commonDestroyManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
try {
|
||||
const records =
|
||||
await this.commonDestroyManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return records.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
return records.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
DestroyManyResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+47
-18
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import graphqlFields from 'graphql-fields';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type DestroyOneResolverArgs,
|
||||
@@ -11,8 +12,11 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works
|
||||
|
||||
import { CommonDestroyOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class DestroyOneResolverFactory
|
||||
@@ -21,7 +25,9 @@ export class DestroyOneResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.DESTROY_ONE;
|
||||
|
||||
constructor(
|
||||
private readonly graphQLQueryRunnerService: GraphqlQueryDestroyOneResolverService,
|
||||
private readonly commonDestroyOneQueryRunnerService: CommonDestroyOneQueryRunnerService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
create(
|
||||
@@ -30,29 +36,52 @@ export class DestroyOneResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: internalContext.authContext.workspace?.id as string,
|
||||
});
|
||||
|
||||
try {
|
||||
const record = await this.commonDestroyOneQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
try {
|
||||
const record = await this.commonDestroyOneQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphQLQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
DestroyOneResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+55
-26
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { OrderByDirection } from 'twenty-shared/types';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type FindDuplicatesResolverArgs,
|
||||
@@ -12,8 +13,11 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works
|
||||
|
||||
import { CommonFindDuplicatesQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class FindDuplicatesResolverFactory
|
||||
@@ -22,7 +26,9 @@ export class FindDuplicatesResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.FIND_DUPLICATES;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryFindDuplicatesResolverService,
|
||||
private readonly commonFindDuplicatesQueryRunnerService: CommonFindDuplicatesQueryRunnerService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
create(
|
||||
@@ -31,35 +37,58 @@ export class FindDuplicatesResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: internalContext.authContext.workspace?.id as string,
|
||||
});
|
||||
|
||||
try {
|
||||
const paginatedDuplicates =
|
||||
await this.commonFindDuplicatesQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||
|
||||
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
try {
|
||||
const paginatedDuplicates =
|
||||
await this.commonFindDuplicatesQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return paginatedDuplicates.map((duplicate) =>
|
||||
typeORMObjectRecordsParser.createConnection({
|
||||
objectRecords: duplicate.records,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: duplicate.records.length,
|
||||
totalCount: duplicate.totalCount,
|
||||
order: [{ id: OrderByDirection.AscNullsFirst }],
|
||||
hasNextPage: duplicate.hasNextPage,
|
||||
hasPreviousPage: duplicate.hasPreviousPage,
|
||||
}),
|
||||
);
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return paginatedDuplicates.map((duplicate) =>
|
||||
typeORMObjectRecordsParser.createConnection({
|
||||
objectRecords: duplicate.records,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: duplicate.records.length,
|
||||
totalCount: duplicate.totalCount,
|
||||
order: [{ id: OrderByDirection.AscNullsFirst }],
|
||||
hasNextPage: duplicate.hasNextPage,
|
||||
hasPreviousPage: duplicate.hasPreviousPage,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
FindDuplicatesResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+53
-29
@@ -12,8 +12,11 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
|
||||
|
||||
import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
|
||||
@Injectable()
|
||||
export class FindManyResolverFactory
|
||||
@@ -23,6 +26,8 @@ export class FindManyResolverFactory
|
||||
|
||||
constructor(
|
||||
private readonly commonFindManyQueryRunnerService: CommonFindManyQueryRunnerService,
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryFindManyResolverService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
@@ -31,40 +36,59 @@ export class FindManyResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
internalContext.authContext.workspace?.id as string,
|
||||
);
|
||||
|
||||
try {
|
||||
const {
|
||||
records,
|
||||
aggregatedValues,
|
||||
totalCount,
|
||||
pageInfo,
|
||||
selectedFieldsResult,
|
||||
} = await this.commonFindManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
if (isCommonApiEnabled) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
try {
|
||||
const {
|
||||
records,
|
||||
aggregatedValues,
|
||||
totalCount,
|
||||
pageInfo,
|
||||
selectedFieldsResult,
|
||||
} = await this.commonFindManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.createConnection({
|
||||
objectRecords: records,
|
||||
objectRecordsAggregatedValues: aggregatedValues,
|
||||
selectedAggregatedFields: selectedFieldsResult.aggregate,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: args.first ?? args.last ?? QUERY_MAX_RECORDS,
|
||||
totalCount,
|
||||
order: args.orderBy,
|
||||
hasNextPage: pageInfo.hasNextPage,
|
||||
hasPreviousPage: pageInfo.hasPreviousPage,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.createConnection({
|
||||
objectRecords: records,
|
||||
objectRecordsAggregatedValues: aggregatedValues,
|
||||
selectedAggregatedFields: selectedFieldsResult.aggregate,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: args.first ?? args.last ?? QUERY_MAX_RECORDS,
|
||||
totalCount,
|
||||
order: args.orderBy,
|
||||
hasNextPage: pageInfo.hasNextPage,
|
||||
hasPreviousPage: pageInfo.hasPreviousPage,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
{
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
},
|
||||
FindManyResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+42
-18
@@ -11,8 +11,11 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
|
||||
|
||||
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
|
||||
@Injectable()
|
||||
export class FindOneResolverFactory
|
||||
@@ -22,6 +25,8 @@ export class FindOneResolverFactory
|
||||
|
||||
constructor(
|
||||
private readonly commonFindOneQueryRunnerService: CommonFindOneQueryRunnerService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryFindOneResolverService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
@@ -30,29 +35,48 @@ export class FindOneResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
try {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
internalContext.authContext.workspace?.id as string,
|
||||
);
|
||||
|
||||
const record = await this.commonFindOneQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
if (isCommonApiEnabled) {
|
||||
try {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
const record = await this.commonFindOneQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
{
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
},
|
||||
FindOneResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+45
-18
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import graphqlFields from 'graphql-fields';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type MergeManyResolverArgs,
|
||||
@@ -11,8 +12,11 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
|
||||
|
||||
import { CommonMergeManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-merge-many-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryMergeManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
|
||||
@Injectable()
|
||||
export class MergeManyResolverFactory
|
||||
@@ -21,7 +25,9 @@ export class MergeManyResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.MERGE_MANY;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryMergeManyResolverService,
|
||||
private readonly commonMergeManyQueryRunnerService: CommonMergeManyQueryRunnerService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
@@ -30,29 +36,50 @@ export class MergeManyResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
internalContext.authContext.workspace?.id as string,
|
||||
);
|
||||
|
||||
try {
|
||||
const record = await this.commonMergeManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
if (isCommonApiEnabled) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
try {
|
||||
const record = await this.commonMergeManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
MergeManyResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+50
-21
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type Resolver,
|
||||
@@ -12,8 +13,11 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works
|
||||
|
||||
import { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-many-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryRestoreManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class RestoreManyResolverFactory
|
||||
@@ -22,6 +26,8 @@ export class RestoreManyResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.RESTORE_MANY;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryRestoreManyResolverService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly commonRestoreManyQueryRunnerService: CommonRestoreManyQueryRunnerService,
|
||||
) {}
|
||||
|
||||
@@ -31,31 +37,54 @@ export class RestoreManyResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: internalContext.authContext.workspace?.id as string,
|
||||
});
|
||||
|
||||
try {
|
||||
const records = await this.commonRestoreManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
try {
|
||||
const records =
|
||||
await this.commonRestoreManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return records.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
return records.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
RestoreManyResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+47
-18
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import graphqlFields from 'graphql-fields';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type Resolver,
|
||||
@@ -11,8 +12,11 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works
|
||||
|
||||
import { CommonRestoreOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-one-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryRestoreOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class RestoreOneResolverFactory
|
||||
@@ -21,6 +25,8 @@ export class RestoreOneResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.RESTORE_ONE;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryRestoreOneResolverService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly commonRestoreOneQueryRunnerService: CommonRestoreOneQueryRunnerService,
|
||||
) {}
|
||||
|
||||
@@ -30,29 +36,52 @@ export class RestoreOneResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: internalContext.authContext.workspace?.id as string,
|
||||
});
|
||||
|
||||
try {
|
||||
const record = await this.commonRestoreOneQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
try {
|
||||
const record = await this.commonRestoreOneQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
RestoreOneResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+49
-20
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type Resolver,
|
||||
@@ -12,8 +13,11 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works
|
||||
|
||||
import { CommonUpdateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-many-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateManyResolverFactory
|
||||
@@ -22,7 +26,9 @@ export class UpdateManyResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.UPDATE_MANY;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryUpdateManyResolverService,
|
||||
private readonly commonUpdateManyQueryRunnerService: CommonUpdateManyQueryRunnerService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
create(
|
||||
@@ -31,31 +37,54 @@ export class UpdateManyResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: internalContext.authContext.workspace?.id as string,
|
||||
});
|
||||
|
||||
try {
|
||||
const records = await this.commonUpdateManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
try {
|
||||
const records = await this.commonUpdateManyQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
return records.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return records.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
UpdateManyResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+47
-18
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import graphqlFields from 'graphql-fields';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import {
|
||||
type Resolver,
|
||||
@@ -11,8 +12,11 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works
|
||||
|
||||
import { CommonUpdateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-one-query-runner.service';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateOneResolverFactory
|
||||
@@ -21,7 +25,9 @@ export class UpdateOneResolverFactory
|
||||
public static methodName = RESOLVER_METHOD_NAMES.UPDATE_ONE;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryUpdateOneResolverService,
|
||||
private readonly commonUpdateOneQueryRunnerService: CommonUpdateOneQueryRunnerService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
create(
|
||||
@@ -30,29 +36,52 @@ export class UpdateOneResolverFactory
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, _context, info) => {
|
||||
const selectedFields = graphqlFields(info);
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: internalContext.authContext.workspace?.id as string,
|
||||
});
|
||||
|
||||
try {
|
||||
const record = await this.commonUpdateOneQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) {
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
try {
|
||||
const record = await this.commonUpdateOneQueryRunnerService.execute(
|
||||
{ ...args, selectedFields },
|
||||
internalContext,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(
|
||||
internalContext.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord({
|
||||
objectRecord: record,
|
||||
objectName:
|
||||
internalContext.objectMetadataItemWithFieldMaps.nameSingular,
|
||||
take: 1,
|
||||
totalCount: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
const options: WorkspaceQueryRunnerOptions = {
|
||||
authContext: internalContext.authContext,
|
||||
info,
|
||||
objectMetadataMaps: internalContext.objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps:
|
||||
internalContext.objectMetadataItemWithFieldMaps,
|
||||
};
|
||||
|
||||
return await this.graphqlQueryRunnerService.execute(
|
||||
args,
|
||||
options,
|
||||
UpdateOneResolverFactory.methodName,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
import { BadRequestException, Inject } from '@nestjs/common';
|
||||
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import {
|
||||
assertIsDefinedOrThrow,
|
||||
getSettingsPath,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||
|
||||
import { CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
|
||||
import { CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type';
|
||||
import { RestToCommonSelectedFieldsHandler } from 'src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler';
|
||||
import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils';
|
||||
import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { getObjectMetadataMapItemByNamePlural } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
|
||||
import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util';
|
||||
|
||||
export interface PageInfo {
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
startCursor: string | null;
|
||||
endCursor: string | null;
|
||||
}
|
||||
|
||||
export interface FormatResult {
|
||||
data?: {
|
||||
[operation: string]: object;
|
||||
};
|
||||
pageInfo?: PageInfo;
|
||||
totalCount?: number;
|
||||
}
|
||||
|
||||
export abstract class RestApiBaseHandler {
|
||||
@Inject()
|
||||
protected readonly recordInputTransformerService: RecordInputTransformerService;
|
||||
@Inject()
|
||||
protected readonly twentyORMManager: TwentyORMManager;
|
||||
@Inject()
|
||||
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;
|
||||
@Inject()
|
||||
protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService;
|
||||
@Inject()
|
||||
protected readonly workspaceCacheStorageService: WorkspaceCacheStorageService;
|
||||
@Inject()
|
||||
protected readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService;
|
||||
@Inject()
|
||||
protected readonly apiKeyRoleService: ApiKeyRoleService;
|
||||
@Inject()
|
||||
protected readonly restToCommonSelectedFieldsHandler: RestToCommonSelectedFieldsHandler;
|
||||
@Inject()
|
||||
protected readonly userRoleService: UserRoleService;
|
||||
@Inject()
|
||||
protected readonly accessTokenService: AccessTokenService;
|
||||
@Inject()
|
||||
protected readonly workspaceDomainsService: WorkspaceDomainsService;
|
||||
@Inject()
|
||||
protected readonly featureFlagService: FeatureFlagService;
|
||||
|
||||
protected abstract handle(
|
||||
request: AuthenticatedRequest,
|
||||
): Promise<
|
||||
FormatResult | { data: FormatResult[] } | CommonGroupByOutputItem[]
|
||||
>;
|
||||
|
||||
public getAuthContextFromRequest(
|
||||
request: AuthenticatedRequest,
|
||||
): WorkspaceAuthContext {
|
||||
return request;
|
||||
}
|
||||
|
||||
private getObjectsPermissions = async (authContext: WorkspaceAuthContext) => {
|
||||
let roleId: string;
|
||||
|
||||
if (isDefined(authContext.apiKey)) {
|
||||
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
|
||||
authContext.apiKey.id,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
} else {
|
||||
const userWorkspaceRoleId =
|
||||
await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
workspaceId: authContext.workspace.id,
|
||||
});
|
||||
|
||||
if (!isDefined(userWorkspaceRoleId)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
);
|
||||
}
|
||||
|
||||
roleId = userWorkspaceRoleId;
|
||||
}
|
||||
|
||||
const objectMetadataPermissions =
|
||||
await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles(
|
||||
{
|
||||
workspaceId: authContext.workspace.id,
|
||||
roleIds: [roleId],
|
||||
},
|
||||
);
|
||||
|
||||
return { objectsPermissions: objectMetadataPermissions[roleId] };
|
||||
};
|
||||
|
||||
async computeSelectedFields({
|
||||
authContext,
|
||||
depth,
|
||||
objectMetadataMapItem,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
authContext: WorkspaceAuthContext;
|
||||
depth?: Depth | undefined;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
}): Promise<CommonSelectedFields> {
|
||||
const { objectsPermissions } =
|
||||
await this.getObjectsPermissions(authContext);
|
||||
|
||||
return this.restToCommonSelectedFieldsHandler.computeFromDepth({
|
||||
objectsPermissions,
|
||||
objectMetadataMaps,
|
||||
objectMetadataMapItem,
|
||||
depth,
|
||||
});
|
||||
}
|
||||
|
||||
async buildCommonOptions(request: AuthenticatedRequest) {
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
|
||||
const { objectMetadataMaps, objectMetadataMapItem } =
|
||||
await this.getObjectMetadata(request, parsedObject);
|
||||
|
||||
const authContext = this.getAuthContextFromRequest(request);
|
||||
|
||||
return {
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps: objectMetadataMapItem,
|
||||
objectMetadataMaps: objectMetadataMaps,
|
||||
};
|
||||
}
|
||||
|
||||
private async getObjectMetadata(
|
||||
request: AuthenticatedRequest,
|
||||
parsedObject: string,
|
||||
): Promise<{
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
}> {
|
||||
const { workspace } =
|
||||
await this.accessTokenService.validateTokenByRequest(request);
|
||||
|
||||
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
|
||||
|
||||
const currentCacheVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(workspace.id);
|
||||
|
||||
if (currentCacheVersion === undefined) {
|
||||
await this.workspaceMetadataCacheService.recomputeMetadataCache({
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
throw new BadRequestException('Metadata cache version not found');
|
||||
}
|
||||
const objectMetadataMaps =
|
||||
await this.workspaceCacheStorageService.getObjectMetadataMaps(
|
||||
workspace.id,
|
||||
currentCacheVersion,
|
||||
);
|
||||
|
||||
if (!objectMetadataMaps) {
|
||||
throw new BadRequestException(
|
||||
`No object was found for the workspace associated with this API key. You may generate a new one here ${this.workspaceDomainsService
|
||||
.buildWorkspaceURL({
|
||||
workspace,
|
||||
pathname: getSettingsPath(SettingsPath.ApiWebhooks),
|
||||
})
|
||||
.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataItem = getObjectMetadataMapItemByNamePlural(
|
||||
objectMetadataMaps,
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
if (!objectMetadataItem) {
|
||||
const wrongObjectMetadataItem = getObjectMetadataMapItemByNameSingular(
|
||||
objectMetadataMaps,
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
let hint = 'eg: companies';
|
||||
|
||||
if (wrongObjectMetadataItem) {
|
||||
hint = `Did you mean '${wrongObjectMetadataItem.namePlural}'?`;
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
`object '${parsedObject}' not found. ${hint}`,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceFeatureFlagsMap =
|
||||
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspace.id);
|
||||
|
||||
// Check if this entity is workspace-gated and should be blocked from workspace API
|
||||
if (
|
||||
shouldExcludeFromWorkspaceApi(
|
||||
objectMetadataItem,
|
||||
standardObjectMetadataDefinitions,
|
||||
workspaceFeatureFlagsMap,
|
||||
)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`object '${parsedObject}' not found. ${parsedObject} is not available via REST API.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
objectMetadataMaps,
|
||||
objectMetadataMapItem: objectMetadataItem,
|
||||
};
|
||||
}
|
||||
}
|
||||
+100
-5
@@ -1,14 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { parseUpsertRestRequest } from 'src/engine/api/rest/input-request-parsers/upsert-parser-utils/parse-upsert-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
|
||||
import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils';
|
||||
@Injectable()
|
||||
export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
constructor(
|
||||
@@ -17,7 +24,7 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
super();
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
async commonHandle(request: AuthenticatedRequest) {
|
||||
try {
|
||||
const { data, depth, upsert } = this.parseRequestArgs(request);
|
||||
|
||||
@@ -48,7 +55,7 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
objectMetadataItemWithFieldMaps.namePlural,
|
||||
);
|
||||
} catch (error) {
|
||||
return workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,4 +73,92 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
upsert: parseUpsertRestRequest(request),
|
||||
};
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { objectMetadata, repository, restrictedFields } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const body = request.body;
|
||||
|
||||
if (!Array.isArray(body)) {
|
||||
throw new BadRequestException('Body must be an array');
|
||||
}
|
||||
|
||||
if (body.length === 0) {
|
||||
throw new BadRequestException('Input must not be empty');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const overriddenRecordsToCreate: Record<string, any>[] = [];
|
||||
|
||||
for (const recordToCreate of body) {
|
||||
const overriddenBody = await this.recordInputTransformerService.process({
|
||||
recordInput: recordToCreate,
|
||||
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
|
||||
});
|
||||
|
||||
const recordExists =
|
||||
isDefined(overriddenBody.id) &&
|
||||
(await repository.exists({
|
||||
where: {
|
||||
id: overriddenBody.id,
|
||||
},
|
||||
}));
|
||||
|
||||
if (recordExists) {
|
||||
throw new BadRequestException('Record already exists');
|
||||
}
|
||||
|
||||
overriddenRecordsToCreate.push(overriddenBody);
|
||||
}
|
||||
|
||||
const recordsToCreate =
|
||||
await this.createdByFromAuthContextService.injectCreatedBy(
|
||||
overriddenRecordsToCreate,
|
||||
objectMetadata.objectMetadataMapItem.nameSingular,
|
||||
this.getAuthContextFromRequest(request),
|
||||
);
|
||||
|
||||
let selectedColumns = undefined;
|
||||
|
||||
if (!isEmpty(restrictedFields)) {
|
||||
const selectableFields = getAllSelectableColumnNames({
|
||||
restrictedFields,
|
||||
objectMetadata,
|
||||
});
|
||||
|
||||
selectedColumns = Object.keys(selectableFields).filter(
|
||||
(key) => selectableFields[key],
|
||||
);
|
||||
}
|
||||
|
||||
const createdRecords = await repository.insert(
|
||||
recordsToCreate,
|
||||
undefined,
|
||||
selectedColumns,
|
||||
);
|
||||
const createdRecordsIds = createdRecords.identifiers.map(
|
||||
(record) => record.id,
|
||||
);
|
||||
|
||||
const records = await this.getRecord({
|
||||
recordIds: createdRecordsIds,
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: parseDepthRestRequest(request),
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
if (records.length !== body.length) {
|
||||
throw new InternalServerErrorException(
|
||||
`Error when creating records. ${body.length - records.length} records are missing after creation.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'create',
|
||||
objectNamePlural: objectMetadata.objectMetadataMapItem.namePlural,
|
||||
data: records,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+81
-5
@@ -1,14 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { parseUpsertRestRequest } from 'src/engine/api/rest/input-request-parsers/upsert-parser-utils/parse-upsert-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
|
||||
import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
@@ -18,7 +25,7 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
super();
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
async commonHandle(request: AuthenticatedRequest) {
|
||||
try {
|
||||
const { data, depth, upsert } = this.parseRequestArgs(request);
|
||||
|
||||
@@ -49,7 +56,7 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
} catch (error) {
|
||||
return workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,4 +71,73 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
upsert: parseUpsertRestRequest(request),
|
||||
};
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { objectMetadata, repository, restrictedFields } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const overriddenBody = await this.recordInputTransformerService.process({
|
||||
recordInput: request.body,
|
||||
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
|
||||
});
|
||||
|
||||
const recordExists =
|
||||
isDefined(overriddenBody.id) &&
|
||||
(await repository.exists({
|
||||
where: {
|
||||
id: overriddenBody.id,
|
||||
},
|
||||
}));
|
||||
|
||||
if (recordExists) {
|
||||
throw new BadRequestException('Record already exists');
|
||||
}
|
||||
|
||||
const [recordToCreate] =
|
||||
await this.createdByFromAuthContextService.injectCreatedBy(
|
||||
[overriddenBody],
|
||||
objectMetadata.objectMetadataMapItem.nameSingular,
|
||||
this.getAuthContextFromRequest(request),
|
||||
);
|
||||
|
||||
let selectedColumns = undefined;
|
||||
|
||||
if (!isEmpty(restrictedFields)) {
|
||||
const selectableFields = getAllSelectableColumnNames({
|
||||
restrictedFields,
|
||||
objectMetadata,
|
||||
});
|
||||
|
||||
selectedColumns = Object.keys(selectableFields).filter(
|
||||
(key) => selectableFields[key],
|
||||
);
|
||||
}
|
||||
|
||||
const createdRecordResult = await repository.insert(
|
||||
recordToCreate,
|
||||
undefined,
|
||||
selectedColumns,
|
||||
);
|
||||
const createdRecord = createdRecordResult.identifiers[0];
|
||||
|
||||
const records = await this.getRecord({
|
||||
recordIds: [createdRecord.id],
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: parseDepthRestRequest(request),
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
const record = records[0];
|
||||
|
||||
if (!isDefined(record)) {
|
||||
throw new InternalServerErrorException('Created record not found');
|
||||
}
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'create',
|
||||
objectNameSingular: objectMetadata.objectMetadataMapItem.nameSingular,
|
||||
data: record,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service';
|
||||
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
+3
-2
@@ -3,9 +3,10 @@ import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonDeleteOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-one-query-runner.service';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
|
||||
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service';
|
||||
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
+39
-4
@@ -3,11 +3,13 @@ import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonDestroyOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
|
||||
import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiDestroyOneHandler extends RestApiBaseHandler {
|
||||
@@ -17,7 +19,7 @@ export class RestApiDestroyOneHandler extends RestApiBaseHandler {
|
||||
super();
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
async commonHandle(request: AuthenticatedRequest) {
|
||||
try {
|
||||
const { id } = this.parseRequestArgs(request);
|
||||
|
||||
@@ -41,7 +43,7 @@ export class RestApiDestroyOneHandler extends RestApiBaseHandler {
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
} catch (error) {
|
||||
return workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,4 +62,37 @@ export class RestApiDestroyOneHandler extends RestApiBaseHandler {
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!recordId) {
|
||||
throw new BadRequestException('Record ID not found');
|
||||
}
|
||||
|
||||
const { objectMetadata, repository, restrictedFields } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const selectOptions = getAllSelectableColumnNames({
|
||||
restrictedFields,
|
||||
objectMetadata,
|
||||
});
|
||||
|
||||
const recordToDelete = await repository.findOneOrFail({
|
||||
where: { id: recordId },
|
||||
select: selectOptions,
|
||||
});
|
||||
|
||||
const columnsToReturnForDelete: string[] = [];
|
||||
|
||||
await repository.delete(recordId, undefined, columnsToReturnForDelete);
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'delete',
|
||||
objectNameSingular: objectMetadata.objectMetadataMapItem.nameSingular,
|
||||
data: {
|
||||
id: recordToDelete.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+140
-4
@@ -1,12 +1,20 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import {
|
||||
type FormatResult,
|
||||
RestApiBaseHandler,
|
||||
} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonFindDuplicatesQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
|
||||
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
|
||||
@@ -16,8 +24,10 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
|
||||
super();
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
async commonHandle(request: AuthenticatedRequest) {
|
||||
try {
|
||||
this.validate(request);
|
||||
|
||||
const { data, ids, depth } = this.parseRequestArgs(request);
|
||||
|
||||
const {
|
||||
@@ -48,7 +58,7 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
} catch (error) {
|
||||
return workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,4 +94,130 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
|
||||
depth: parseDepthRestRequest(request),
|
||||
};
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
this.validate(request);
|
||||
|
||||
const {
|
||||
repository,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
} = await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const existingRecordsQueryBuilder = repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldsMaps.nameSingular,
|
||||
);
|
||||
|
||||
let objectRecords: Partial<ObjectRecord>[] = [];
|
||||
|
||||
if (request.body.ids) {
|
||||
objectRecords = (await existingRecordsQueryBuilder
|
||||
.where({ id: In(request.body.ids) })
|
||||
.getMany()) as ObjectRecord[];
|
||||
} else if (request.body.data && !isEmpty(request.body.data)) {
|
||||
objectRecords = request.body.data;
|
||||
}
|
||||
|
||||
const duplicateConditions = objectRecords.map((record) =>
|
||||
buildDuplicateConditions(
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
[record],
|
||||
record.id,
|
||||
),
|
||||
);
|
||||
|
||||
const result: { data: FormatResult[] } = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
for (const duplicateCondition of duplicateConditions) {
|
||||
const {
|
||||
records,
|
||||
isForwardPagination,
|
||||
hasMoreRecords,
|
||||
totalCount,
|
||||
startCursor,
|
||||
endCursor,
|
||||
} = await this.findRecords({
|
||||
request,
|
||||
repository,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
extraFilters: duplicateCondition,
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
const paginatedResult = this.formatPaginatedDuplicatesResult({
|
||||
finalRecords: records,
|
||||
objectMetadataNameSingular:
|
||||
objectMetadata.objectMetadataMapItem.nameSingular,
|
||||
isForwardPagination,
|
||||
hasMoreRecords,
|
||||
totalCount,
|
||||
startCursor,
|
||||
endCursor,
|
||||
});
|
||||
|
||||
result.data.push(paginatedResult);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private validate(request: Request) {
|
||||
const { data, ids } = request.body;
|
||||
|
||||
if (!data && !ids) {
|
||||
throw new BadRequestException(
|
||||
'You have to provide either "data" or "ids" argument',
|
||||
);
|
||||
}
|
||||
|
||||
if (data && ids) {
|
||||
throw new BadRequestException(
|
||||
'You cannot provide both "data" and "ids" arguments',
|
||||
);
|
||||
}
|
||||
|
||||
if (!ids && isEmpty(data)) {
|
||||
throw new BadRequestException(
|
||||
'The "data" condition can not be empty when "ids" input not provided',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
formatPaginatedDuplicatesResult({
|
||||
finalRecords,
|
||||
objectMetadataNameSingular,
|
||||
isForwardPagination,
|
||||
hasMoreRecords,
|
||||
totalCount,
|
||||
startCursor,
|
||||
endCursor,
|
||||
}: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
finalRecords: any[];
|
||||
objectMetadataNameSingular: string;
|
||||
isForwardPagination: boolean;
|
||||
hasMoreRecords: boolean;
|
||||
totalCount: number;
|
||||
startCursor: string | null;
|
||||
endCursor: string | null;
|
||||
}) {
|
||||
const hasPreviousPage = !isForwardPagination && hasMoreRecords;
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'findDuplicates',
|
||||
objectNameSingular: objectMetadataNameSingular,
|
||||
data: isForwardPagination ? finalRecords : finalRecords.reverse(),
|
||||
pageInfo: {
|
||||
hasNextPage: isForwardPagination && hasMoreRecords,
|
||||
...(hasPreviousPage ? { hasPreviousPage } : {}),
|
||||
startCursor,
|
||||
endCursor,
|
||||
},
|
||||
totalCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+71
-2
@@ -5,7 +5,8 @@ import { ObjectRecord } from 'twenty-shared/types';
|
||||
import {
|
||||
PageInfo,
|
||||
RestApiBaseHandler,
|
||||
} from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util';
|
||||
@@ -25,6 +26,74 @@ export class RestApiFindManyHandler extends RestApiBaseHandler {
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const {
|
||||
repository,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
} = await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const {
|
||||
records,
|
||||
isForwardPagination,
|
||||
hasMoreRecords,
|
||||
totalCount,
|
||||
startCursor,
|
||||
endCursor,
|
||||
} = await this.findRecords({
|
||||
request,
|
||||
repository,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
return this.formatPaginatedResult({
|
||||
finalRecords: records,
|
||||
objectMetadataNamePlural: objectMetadata.objectMetadataMapItem.namePlural,
|
||||
isForwardPagination,
|
||||
hasMoreRecords,
|
||||
totalCount,
|
||||
startCursor,
|
||||
endCursor,
|
||||
});
|
||||
}
|
||||
|
||||
formatPaginatedResult({
|
||||
finalRecords,
|
||||
objectMetadataNamePlural,
|
||||
isForwardPagination,
|
||||
hasMoreRecords,
|
||||
totalCount,
|
||||
startCursor,
|
||||
endCursor,
|
||||
}: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
finalRecords: any[];
|
||||
objectMetadataNamePlural: string;
|
||||
isForwardPagination: boolean;
|
||||
hasMoreRecords: boolean;
|
||||
totalCount: number;
|
||||
startCursor: string | null;
|
||||
endCursor: string | null;
|
||||
}) {
|
||||
const hasPreviousPage = !isForwardPagination && hasMoreRecords;
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'findMany',
|
||||
objectNamePlural: objectMetadataNamePlural,
|
||||
data: isForwardPagination ? finalRecords : finalRecords.reverse(),
|
||||
pageInfo: {
|
||||
hasNextPage: isForwardPagination && hasMoreRecords,
|
||||
...(hasPreviousPage ? { hasPreviousPage } : {}),
|
||||
startCursor,
|
||||
endCursor,
|
||||
},
|
||||
totalCount,
|
||||
});
|
||||
}
|
||||
|
||||
async commonHandle(request: AuthenticatedRequest) {
|
||||
try {
|
||||
const parsedArgs = this.parseRequestArgs(request);
|
||||
const {
|
||||
@@ -60,7 +129,7 @@ export class RestApiFindManyHandler extends RestApiBaseHandler {
|
||||
pageInfo,
|
||||
);
|
||||
} catch (error) {
|
||||
return workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+44
-4
@@ -1,11 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
|
||||
|
||||
@@ -18,6 +20,44 @@ export class RestApiFindOneHandler extends RestApiBaseHandler {
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new BadRequestException(
|
||||
'No recordId provided in rest api get one query',
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
repository,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
} = await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const { records } = await this.findRecords({
|
||||
request,
|
||||
recordId,
|
||||
repository,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
const record = records?.[0];
|
||||
|
||||
if (!isDefined(record)) {
|
||||
throw new BadRequestException('Record not found');
|
||||
}
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'findOne',
|
||||
objectNameSingular: objectMetadata.objectMetadataMapItem.nameSingular,
|
||||
data: record,
|
||||
});
|
||||
}
|
||||
|
||||
async commonHandle(request: AuthenticatedRequest) {
|
||||
try {
|
||||
const { filter, depth } = await this.parseRequestArgs(request);
|
||||
const {
|
||||
@@ -47,7 +87,7 @@ export class RestApiFindOneHandler extends RestApiBaseHandler {
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
} catch (error) {
|
||||
return workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service';
|
||||
import { parseAggregateFieldsRestRequest } from 'src/engine/api/rest/input-request-parsers/aggregate-fields-parser-utils/parse-aggregate-fields-rest-request.util';
|
||||
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonMergeManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-merge-many-query-runner.service';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-many-query-runner.service';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
|
||||
|
||||
+3
-2
@@ -3,10 +3,11 @@ import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonRestoreOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-one-query-runner.service';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
|
||||
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonUpdateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-many-query-runner.service';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
|
||||
|
||||
+77
-6
@@ -1,14 +1,21 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { CommonUpdateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-one-query-runner.service';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
|
||||
import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiUpdateOneHandler extends RestApiBaseHandler {
|
||||
@@ -18,7 +25,7 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler {
|
||||
super();
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
async commonHandle(request: AuthenticatedRequest) {
|
||||
try {
|
||||
const { id, data, depth } = this.parseRequestArgs(request);
|
||||
|
||||
@@ -49,7 +56,7 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler {
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
} catch (error) {
|
||||
return workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
workspaceQueryRunnerRestApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,4 +77,68 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler {
|
||||
depth: parseDepthRestRequest(request),
|
||||
};
|
||||
}
|
||||
|
||||
async handle(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (!recordId) {
|
||||
throw new BadRequestException('Record ID not found');
|
||||
}
|
||||
|
||||
const { objectMetadata, repository, restrictedFields } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
// assert the record exists
|
||||
await repository.findOneOrFail({
|
||||
select: { id: true },
|
||||
where: { id: recordId },
|
||||
});
|
||||
|
||||
const overriddenBody = await this.recordInputTransformerService.process({
|
||||
recordInput: request.body,
|
||||
objectMetadataMapItem: objectMetadata.objectMetadataMapItem,
|
||||
});
|
||||
|
||||
let selectedColumns = undefined;
|
||||
|
||||
if (!isEmpty(restrictedFields)) {
|
||||
const selectableFields = getAllSelectableColumnNames({
|
||||
restrictedFields,
|
||||
objectMetadata,
|
||||
});
|
||||
|
||||
selectedColumns = Object.keys(selectableFields).filter(
|
||||
(key) => selectableFields[key],
|
||||
);
|
||||
}
|
||||
|
||||
const updatedRecord = await repository.update(
|
||||
recordId,
|
||||
overriddenBody,
|
||||
undefined,
|
||||
selectedColumns,
|
||||
);
|
||||
|
||||
const updatedRecordId = updatedRecord.generatedMaps[0].id;
|
||||
|
||||
const records = await this.getRecord({
|
||||
recordIds: [updatedRecordId],
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: parseDepthRestRequest(request),
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
const record = records[0];
|
||||
|
||||
if (!isDefined(record)) {
|
||||
throw new InternalServerErrorException('Updated record not found');
|
||||
}
|
||||
|
||||
return this.formatResult({
|
||||
operation: 'update',
|
||||
objectNameSingular: objectMetadata.objectMetadataMapItem.nameSingular,
|
||||
data: record,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,629 @@
|
||||
import { BadRequestException, Inject } from '@nestjs/common';
|
||||
|
||||
import chunk from 'lodash.chunk';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
ObjectRecord,
|
||||
type RestrictedFieldsPermissions,
|
||||
} from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { In, type ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
|
||||
import { CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
|
||||
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
|
||||
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { RestToCommonSelectedFieldsHandler } from 'src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler';
|
||||
import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
||||
import { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant';
|
||||
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
|
||||
import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
|
||||
import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils';
|
||||
import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
|
||||
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
export interface PageInfo {
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
startCursor: string | null;
|
||||
endCursor: string | null;
|
||||
}
|
||||
|
||||
interface FormatResultParams<T> {
|
||||
operation:
|
||||
| 'delete'
|
||||
| 'create'
|
||||
| 'update'
|
||||
| 'findOne'
|
||||
| 'findMany'
|
||||
| 'findDuplicates';
|
||||
objectNameSingular?: string;
|
||||
objectNamePlural?: string;
|
||||
data: T;
|
||||
pageInfo?: PageInfo;
|
||||
totalCount?: number;
|
||||
}
|
||||
|
||||
export interface FormatResult {
|
||||
data?: {
|
||||
[operation: string]: object;
|
||||
};
|
||||
pageInfo?: PageInfo;
|
||||
totalCount?: number;
|
||||
}
|
||||
|
||||
export abstract class RestApiBaseHandler {
|
||||
@Inject()
|
||||
protected readonly recordInputTransformerService: RecordInputTransformerService;
|
||||
@Inject()
|
||||
protected readonly coreQueryBuilderFactory: CoreQueryBuilderFactory;
|
||||
@Inject()
|
||||
protected readonly twentyORMManager: TwentyORMManager;
|
||||
@Inject()
|
||||
protected readonly getVariablesFactory: GetVariablesFactory;
|
||||
@Inject()
|
||||
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;
|
||||
@Inject()
|
||||
protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService;
|
||||
@Inject()
|
||||
protected readonly workspaceCacheStorageService: WorkspaceCacheStorageService;
|
||||
@Inject()
|
||||
protected readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService;
|
||||
@Inject()
|
||||
protected readonly apiKeyRoleService: ApiKeyRoleService;
|
||||
@Inject()
|
||||
protected readonly restToCommonSelectedFieldsHandler: RestToCommonSelectedFieldsHandler;
|
||||
@Inject()
|
||||
protected readonly userRoleService: UserRoleService;
|
||||
|
||||
protected abstract handle(
|
||||
request: AuthenticatedRequest,
|
||||
): Promise<
|
||||
FormatResult | { data: FormatResult[] } | CommonGroupByOutputItem[]
|
||||
>;
|
||||
|
||||
public async getRepositoryAndMetadataOrFail(request: AuthenticatedRequest) {
|
||||
const { workspace, apiKey, userWorkspaceId } = request;
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
|
||||
const objectMetadata = await this.coreQueryBuilderFactory.getObjectMetadata(
|
||||
request,
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
if (!workspace?.id) {
|
||||
throw new BadRequestException('Workspace not found');
|
||||
}
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new BadRequestException('Object metadata not found');
|
||||
}
|
||||
|
||||
const workspaceDataSource = await this.twentyORMManager.getDatasource();
|
||||
|
||||
const objectMetadataNameSingular =
|
||||
objectMetadata.objectMetadataMapItem.nameSingular;
|
||||
|
||||
const objectMetadataItemWithFieldsMaps =
|
||||
getObjectMetadataMapItemByNameSingular(
|
||||
objectMetadata.objectMetadataMaps,
|
||||
objectMetadataNameSingular,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItemWithFieldsMaps)) {
|
||||
throw new BadRequestException(
|
||||
`Object metadata item with name singular ${objectMetadataNameSingular} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
let roleId: string;
|
||||
|
||||
if (isDefined(apiKey)) {
|
||||
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
|
||||
apiKey.id,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (!isDefined(roleId)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.API_KEY_ROLE_NOT_FOUND,
|
||||
PermissionsExceptionCode.API_KEY_ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const userWorkspaceRoleId =
|
||||
await this.workspacePermissionsCacheService.getRoleIdFromUserWorkspaceId(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(userWorkspaceRoleId)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
);
|
||||
}
|
||||
|
||||
roleId = userWorkspaceRoleId;
|
||||
}
|
||||
|
||||
const repository = workspaceDataSource.getRepository<ObjectRecord>(
|
||||
objectMetadataNameSingular,
|
||||
{
|
||||
unionOf: [roleId],
|
||||
},
|
||||
);
|
||||
|
||||
const objectMetadataPermissions =
|
||||
await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
roleIds: roleId ? [roleId] : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(
|
||||
objectMetadataPermissions?.[roleId]?.[
|
||||
objectMetadata.objectMetadataMapItem.id
|
||||
]?.restrictedFields,
|
||||
)
|
||||
) {
|
||||
throw new InternalServerError('Fields permissions not found for role');
|
||||
}
|
||||
|
||||
const restrictedFields =
|
||||
objectMetadataPermissions[roleId][objectMetadata.objectMetadataMapItem.id]
|
||||
.restrictedFields;
|
||||
|
||||
return {
|
||||
objectMetadata,
|
||||
repository,
|
||||
workspaceDataSource,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
isExecutedByApiKey: isDefined(apiKey),
|
||||
authContext: this.getAuthContextFromRequest(request),
|
||||
objectsPermissions: objectMetadataPermissions[roleId],
|
||||
};
|
||||
}
|
||||
|
||||
getRelations({
|
||||
objectMetadata,
|
||||
depth,
|
||||
}: {
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
};
|
||||
depth: Depth | undefined;
|
||||
}) {
|
||||
if (!isDefined(depth) || depth === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const relations: string[] = [];
|
||||
|
||||
Object.values(objectMetadata.objectMetadataMapItem.fieldsById).forEach(
|
||||
(field) => {
|
||||
if (isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) {
|
||||
if (
|
||||
depth === MAX_DEPTH &&
|
||||
isDefined(field.relationTargetObjectMetadataId)
|
||||
) {
|
||||
const relationTargetObjectMetadata =
|
||||
objectMetadata.objectMetadataMaps.byId[
|
||||
field.relationTargetObjectMetadataId
|
||||
];
|
||||
|
||||
if (!isDefined(relationTargetObjectMetadata)) {
|
||||
throw new BadRequestException(
|
||||
`Object metadata relation target not found for relation creation payload`,
|
||||
);
|
||||
}
|
||||
const depth2Relations = this.getRelations({
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: objectMetadata.objectMetadataMaps,
|
||||
objectMetadataMapItem: relationTargetObjectMetadata,
|
||||
},
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
depth2Relations.forEach((depth2Relation) => {
|
||||
relations.push(`${field.name}.${depth2Relation}`);
|
||||
});
|
||||
} else {
|
||||
relations.push(`${field.name}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return relations;
|
||||
}
|
||||
|
||||
async getRecord({
|
||||
recordIds,
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth,
|
||||
restrictedFields,
|
||||
}: {
|
||||
recordIds: string[];
|
||||
repository: WorkspaceRepository<ObjectLiteral>;
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
};
|
||||
depth: Depth | undefined;
|
||||
restrictedFields: RestrictedFieldsPermissions;
|
||||
}) {
|
||||
const relations = this.getRelations({
|
||||
objectMetadata,
|
||||
depth: depth,
|
||||
});
|
||||
|
||||
const relationsChunk = chunk(relations, 50);
|
||||
|
||||
let selectOptions = undefined;
|
||||
|
||||
if (!isEmpty(restrictedFields)) {
|
||||
selectOptions = getAllSelectableColumnNames({
|
||||
restrictedFields,
|
||||
objectMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
const recordsWithoutRelations = await repository.find({
|
||||
...(selectOptions && { select: selectOptions }),
|
||||
where: { id: In(recordIds) },
|
||||
});
|
||||
|
||||
const recordsMap = new Map(
|
||||
recordsWithoutRelations.map((record) => [record.id, record]),
|
||||
);
|
||||
|
||||
for (const relationChunk of relationsChunk) {
|
||||
const records = await repository.find({
|
||||
...(selectOptions && { select: selectOptions }),
|
||||
where: { id: In(recordIds) },
|
||||
relations: relationChunk,
|
||||
});
|
||||
|
||||
records.map((record) => {
|
||||
recordsMap.set(record.id, {
|
||||
...recordsMap.get(record.id),
|
||||
...record,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const orderedRecords = recordIds.map((id) => recordsMap.get(id));
|
||||
|
||||
return orderedRecords;
|
||||
}
|
||||
|
||||
public getAuthContextFromRequest(
|
||||
request: AuthenticatedRequest,
|
||||
): WorkspaceAuthContext {
|
||||
return request;
|
||||
}
|
||||
|
||||
public formatResult<T>({
|
||||
operation,
|
||||
objectNameSingular,
|
||||
objectNamePlural,
|
||||
data,
|
||||
pageInfo,
|
||||
totalCount,
|
||||
}: FormatResultParams<T>) {
|
||||
let prefix: string;
|
||||
|
||||
if (isDefined(objectNameSingular) && isDefined(objectNamePlural)) {
|
||||
throw new Error(
|
||||
'Cannot define both objectNameSingular and objectNamePlural',
|
||||
);
|
||||
}
|
||||
|
||||
if (operation === 'findOne') {
|
||||
prefix = objectNameSingular || '';
|
||||
} else if (operation === 'findMany') {
|
||||
prefix = objectNamePlural || '';
|
||||
} else if (operation === 'findDuplicates') {
|
||||
prefix = `${objectNameSingular}Duplicates`;
|
||||
} else {
|
||||
prefix =
|
||||
operation + capitalize(objectNameSingular || objectNamePlural || '');
|
||||
}
|
||||
|
||||
return {
|
||||
...(operation === 'findDuplicates'
|
||||
? {
|
||||
[prefix]: data,
|
||||
}
|
||||
: {
|
||||
data: {
|
||||
[prefix]: data,
|
||||
},
|
||||
}),
|
||||
...(isDefined(pageInfo) ? { pageInfo } : {}),
|
||||
...(isDefined(totalCount) ? { totalCount } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async findRecords({
|
||||
request,
|
||||
recordId,
|
||||
repository,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
extraFilters,
|
||||
restrictedFields,
|
||||
}: {
|
||||
request: AuthenticatedRequest;
|
||||
recordId?: string;
|
||||
repository: WorkspaceRepository<ObjectLiteral>;
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
};
|
||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
|
||||
extraFilters?: Partial<ObjectRecordFilter>;
|
||||
restrictedFields: RestrictedFieldsPermissions;
|
||||
}) {
|
||||
const objectMetadataNameSingular =
|
||||
objectMetadata.objectMetadataMapItem.nameSingular;
|
||||
|
||||
const qb = repository
|
||||
.createQueryBuilder(objectMetadataNameSingular)
|
||||
.select('id');
|
||||
|
||||
const inputs = this.getVariablesFactory.create(
|
||||
recordId,
|
||||
request,
|
||||
objectMetadata,
|
||||
);
|
||||
|
||||
const isForwardPagination = !inputs.endingBefore;
|
||||
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
objectMetadata.objectMetadataMaps,
|
||||
);
|
||||
|
||||
const filters = this.computeFilters({
|
||||
inputs,
|
||||
objectMetadata,
|
||||
isForwardPagination,
|
||||
extraFilters,
|
||||
});
|
||||
|
||||
let selectQueryBuilder = isDefined(filters)
|
||||
? graphqlQueryParser.applyFilterToBuilder(
|
||||
qb,
|
||||
objectMetadataNameSingular,
|
||||
filters,
|
||||
)
|
||||
: qb;
|
||||
|
||||
const totalCount = await this.getTotalCount(selectQueryBuilder);
|
||||
|
||||
selectQueryBuilder = graphqlQueryParser.applyOrderToBuilder(
|
||||
selectQueryBuilder,
|
||||
inputs.orderBy || [],
|
||||
objectMetadataNameSingular,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
if (inputs.first) {
|
||||
selectQueryBuilder = selectQueryBuilder.limit(inputs.first);
|
||||
}
|
||||
|
||||
if (inputs.last) {
|
||||
selectQueryBuilder = selectQueryBuilder.limit(inputs.last);
|
||||
}
|
||||
|
||||
const recordIds = await selectQueryBuilder
|
||||
.select(`${objectMetadataNameSingular}.id`)
|
||||
.getMany();
|
||||
|
||||
const records = await this.getRecord({
|
||||
recordIds: recordIds.map((record) => record.id),
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: parseDepthRestRequest(request),
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
const hasMoreRecords = records.length < totalCount;
|
||||
|
||||
const finalRecords = formatGetManyData<ObjectRecord[]>(
|
||||
records,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
objectMetadata.objectMetadataMaps,
|
||||
);
|
||||
|
||||
const startCursor =
|
||||
finalRecords.length > 0
|
||||
? encodeCursor(finalRecords[0], inputs.orderBy)
|
||||
: null;
|
||||
|
||||
const endCursor =
|
||||
finalRecords.length > 0
|
||||
? encodeCursor(finalRecords[finalRecords.length - 1], inputs.orderBy)
|
||||
: null;
|
||||
|
||||
return {
|
||||
records: finalRecords,
|
||||
totalCount,
|
||||
hasMoreRecords,
|
||||
isForwardPagination,
|
||||
startCursor,
|
||||
endCursor,
|
||||
};
|
||||
}
|
||||
|
||||
async getTotalCount(
|
||||
query: WorkspaceSelectQueryBuilder<ObjectLiteral>,
|
||||
): Promise<number> {
|
||||
const countQuery = query.clone();
|
||||
|
||||
return await countQuery.getCount();
|
||||
}
|
||||
|
||||
computeFilters({
|
||||
inputs,
|
||||
objectMetadata,
|
||||
isForwardPagination,
|
||||
extraFilters,
|
||||
}: {
|
||||
inputs: QueryVariables;
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
};
|
||||
isForwardPagination: boolean;
|
||||
extraFilters?: Partial<ObjectRecordFilter>;
|
||||
}) {
|
||||
let appliedFilters = inputs.filter;
|
||||
|
||||
if (extraFilters) {
|
||||
appliedFilters = (appliedFilters
|
||||
? { and: [appliedFilters, extraFilters] }
|
||||
: extraFilters) as unknown as ObjectRecordFilter;
|
||||
}
|
||||
|
||||
const cursor = inputs.startingAfter || inputs.endingBefore;
|
||||
|
||||
if (cursor) {
|
||||
const cursorArgFilter = computeCursorArgFilter(
|
||||
this.parseCursor(cursor),
|
||||
inputs.orderBy || [],
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
appliedFilters = (appliedFilters
|
||||
? {
|
||||
and: [appliedFilters, { or: cursorArgFilter }],
|
||||
}
|
||||
: { or: cursorArgFilter }) as unknown as ObjectRecordFilter;
|
||||
}
|
||||
|
||||
return appliedFilters;
|
||||
}
|
||||
|
||||
private parseCursor = (cursor: string) => {
|
||||
try {
|
||||
return JSON.parse(Buffer.from(cursor ?? '', 'base64').toString());
|
||||
} catch {
|
||||
throw new BadRequestException(`Invalid cursor: ${cursor}`);
|
||||
}
|
||||
};
|
||||
|
||||
private getObjectsPermissions = async (authContext: WorkspaceAuthContext) => {
|
||||
let roleId: string;
|
||||
|
||||
if (isDefined(authContext.apiKey)) {
|
||||
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
|
||||
authContext.apiKey.id,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
} else {
|
||||
const userWorkspaceRoleId =
|
||||
await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
workspaceId: authContext.workspace.id,
|
||||
});
|
||||
|
||||
if (!isDefined(userWorkspaceRoleId)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
);
|
||||
}
|
||||
|
||||
roleId = userWorkspaceRoleId;
|
||||
}
|
||||
|
||||
const objectMetadataPermissions =
|
||||
await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles(
|
||||
{
|
||||
workspaceId: authContext.workspace.id,
|
||||
roleIds: [roleId],
|
||||
},
|
||||
);
|
||||
|
||||
return { objectsPermissions: objectMetadataPermissions[roleId] };
|
||||
};
|
||||
|
||||
async computeSelectedFields({
|
||||
authContext,
|
||||
depth,
|
||||
objectMetadataMapItem,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
authContext: WorkspaceAuthContext;
|
||||
depth?: Depth | undefined;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
}): Promise<CommonSelectedFields> {
|
||||
const { objectsPermissions } =
|
||||
await this.getObjectsPermissions(authContext);
|
||||
|
||||
return this.restToCommonSelectedFieldsHandler.computeFromDepth({
|
||||
objectsPermissions,
|
||||
objectMetadataMaps,
|
||||
objectMetadataMapItem,
|
||||
depth,
|
||||
});
|
||||
}
|
||||
|
||||
async buildCommonOptions(request: AuthenticatedRequest) {
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
|
||||
const { objectMetadataMaps, objectMetadataMapItem } =
|
||||
await this.coreQueryBuilderFactory.getObjectMetadata(
|
||||
request,
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
const authContext = this.getAuthContextFromRequest(request);
|
||||
|
||||
return {
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps: objectMetadataMapItem,
|
||||
objectMetadataMaps: objectMetadataMaps,
|
||||
};
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { assertIsDefinedOrThrow, getSettingsPath } from 'twenty-shared/utils';
|
||||
|
||||
import { CreateManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-many-query.factory';
|
||||
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
|
||||
import { FindDuplicatesQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory';
|
||||
import { FindDuplicatesVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory';
|
||||
import { computeDepth } from 'src/engine/api/rest/core/query-builder/utils/compute-depth.utils';
|
||||
import { parseCoreBatchPath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-batch-path.utils';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { type Query } from 'src/engine/api/rest/core/types/query.type';
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { getObjectMetadataMapItemByNamePlural } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
|
||||
import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util';
|
||||
|
||||
@Injectable()
|
||||
export class CoreQueryBuilderFactory {
|
||||
constructor(
|
||||
private readonly createManyQueryFactory: CreateManyQueryFactory,
|
||||
private readonly createVariablesFactory: CreateVariablesFactory,
|
||||
private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory,
|
||||
private readonly findDuplicatesVariablesFactory: FindDuplicatesVariablesFactory,
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly workspaceDomainsService: WorkspaceDomainsService,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
async getObjectMetadata(
|
||||
request: Request,
|
||||
parsedObject: string,
|
||||
): Promise<{
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
}> {
|
||||
const { workspace } =
|
||||
await this.accessTokenService.validateTokenByRequest(request);
|
||||
|
||||
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
|
||||
|
||||
const currentCacheVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(workspace.id);
|
||||
|
||||
if (currentCacheVersion === undefined) {
|
||||
await this.workspaceMetadataCacheService.recomputeMetadataCache({
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
throw new BadRequestException('Metadata cache version not found');
|
||||
}
|
||||
const objectMetadataMaps =
|
||||
await this.workspaceCacheStorageService.getObjectMetadataMaps(
|
||||
workspace.id,
|
||||
currentCacheVersion,
|
||||
);
|
||||
|
||||
if (!objectMetadataMaps) {
|
||||
throw new BadRequestException(
|
||||
`No object was found for the workspace associated with this API key. You may generate a new one here ${this.workspaceDomainsService
|
||||
.buildWorkspaceURL({
|
||||
workspace,
|
||||
pathname: getSettingsPath(SettingsPath.ApiWebhooks),
|
||||
})
|
||||
.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataItem = getObjectMetadataMapItemByNamePlural(
|
||||
objectMetadataMaps,
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
if (!objectMetadataItem) {
|
||||
const wrongObjectMetadataItem = getObjectMetadataMapItemByNameSingular(
|
||||
objectMetadataMaps,
|
||||
parsedObject,
|
||||
);
|
||||
|
||||
let hint = 'eg: companies';
|
||||
|
||||
if (wrongObjectMetadataItem) {
|
||||
hint = `Did you mean '${wrongObjectMetadataItem.namePlural}'?`;
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
`object '${parsedObject}' not found. ${hint}`,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceFeatureFlagsMap =
|
||||
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspace.id);
|
||||
|
||||
// Check if this entity is workspace-gated and should be blocked from workspace API
|
||||
if (
|
||||
shouldExcludeFromWorkspaceApi(
|
||||
objectMetadataItem,
|
||||
standardObjectMetadataDefinitions,
|
||||
workspaceFeatureFlagsMap,
|
||||
)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`object '${parsedObject}' not found. ${parsedObject} is not available via REST API.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
objectMetadataMaps,
|
||||
objectMetadataMapItem: objectMetadataItem,
|
||||
};
|
||||
}
|
||||
|
||||
async createMany(request: Request): Promise<Query> {
|
||||
const { object: parsedObject } = parseCoreBatchPath(request);
|
||||
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
|
||||
const depth = computeDepth(request);
|
||||
|
||||
return {
|
||||
query: this.createManyQueryFactory.create(objectMetadata, depth),
|
||||
variables: this.createVariablesFactory.create(request),
|
||||
};
|
||||
}
|
||||
|
||||
async findDuplicates(request: Request): Promise<Query> {
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
const objectMetadata = await this.getObjectMetadata(request, parsedObject);
|
||||
const depth = computeDepth(request);
|
||||
|
||||
return {
|
||||
query: this.findDuplicatesQueryFactory.create(objectMetadata, depth),
|
||||
variables: this.findDuplicatesVariablesFactory.create(request),
|
||||
};
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
|
||||
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AuthModule,
|
||||
ApiKeyModule,
|
||||
WorkspaceDomainsModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataCacheModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
providers: [...coreQueryBuilderFactories, CoreQueryBuilderFactory],
|
||||
exports: [CoreQueryBuilderFactory],
|
||||
})
|
||||
export class CoreQueryBuilderModule {}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
@Injectable()
|
||||
export class CreateManyQueryFactory {
|
||||
create(
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
},
|
||||
depth?: number,
|
||||
): string {
|
||||
const objectNamePlural = capitalize(
|
||||
objectMetadata.objectMetadataMapItem.namePlural,
|
||||
);
|
||||
const objectNameSingular = capitalize(
|
||||
objectMetadata.objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
|
||||
return `
|
||||
mutation Create${objectNamePlural}($data: [${objectNameSingular}CreateInput!]) {
|
||||
create${objectNamePlural}(data: $data) {
|
||||
id
|
||||
${Object.values(objectMetadata.objectMetadataMapItem.fieldsById)
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadata.objectMetadataMaps,
|
||||
field,
|
||||
depth,
|
||||
),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
||||
|
||||
@Injectable()
|
||||
export class CreateVariablesFactory {
|
||||
create(request: Request): QueryVariables {
|
||||
return {
|
||||
data: request.body,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { CreateManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-many-query.factory';
|
||||
import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory';
|
||||
import { FindDuplicatesQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory';
|
||||
import { FindDuplicatesVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory';
|
||||
import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory';
|
||||
import { inputFactories } from 'src/engine/api/rest/input-factories/factories';
|
||||
|
||||
export const coreQueryBuilderFactories = [
|
||||
CreateManyQueryFactory,
|
||||
FindDuplicatesQueryFactory,
|
||||
CreateVariablesFactory,
|
||||
GetVariablesFactory,
|
||||
FindDuplicatesVariablesFactory,
|
||||
...inputFactories,
|
||||
];
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
@Injectable()
|
||||
export class FindDuplicatesQueryFactory {
|
||||
create(
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
},
|
||||
depth?: number,
|
||||
): string {
|
||||
const objectNameSingular =
|
||||
objectMetadata.objectMetadataMapItem.nameSingular;
|
||||
|
||||
return `
|
||||
query FindDuplicate${capitalize(
|
||||
objectNameSingular,
|
||||
)}($ids: [UUID], $data: [${capitalize(objectNameSingular)}CreateInput]) {
|
||||
${objectNameSingular}Duplicates(ids: $ids, data: $data) {
|
||||
totalCount
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
edges{
|
||||
node {
|
||||
${Object.values(objectMetadata.objectMetadataMapItem.fieldsById)
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadata.objectMetadataMaps,
|
||||
field,
|
||||
depth,
|
||||
),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
||||
|
||||
@Injectable()
|
||||
export class FindDuplicatesVariablesFactory {
|
||||
create(request: Request): QueryVariables {
|
||||
return request.body;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type';
|
||||
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
|
||||
import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
||||
import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util';
|
||||
import { parseLimitRestRequest } from 'src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util';
|
||||
import { parseStartingAfterRestRequest } from 'src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
@Injectable()
|
||||
export class GetVariablesFactory {
|
||||
constructor(
|
||||
private readonly orderByInputFactory: OrderByInputFactory,
|
||||
private readonly filterInputFactory: FilterInputFactory,
|
||||
) {}
|
||||
|
||||
create(
|
||||
id: string | undefined,
|
||||
request: AuthenticatedRequest,
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
},
|
||||
): QueryVariables {
|
||||
if (isDefined(id)) {
|
||||
return { filter: { id: { eq: id } } };
|
||||
}
|
||||
|
||||
const filter = this.filterInputFactory.create(request, objectMetadata);
|
||||
const limit = parseLimitRestRequest(request);
|
||||
const orderBy = this.orderByInputFactory.create(request, objectMetadata);
|
||||
const endingBefore = parseEndingBeforeRestRequest(request);
|
||||
const startingAfter = parseStartingAfterRestRequest(request);
|
||||
|
||||
return {
|
||||
filter,
|
||||
orderBy,
|
||||
first: !endingBefore ? limit : undefined,
|
||||
last: endingBefore ? limit : undefined,
|
||||
startingAfter,
|
||||
endingBefore,
|
||||
};
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`checkFields should accept valid relation field name 1`] = `"field 'pointOfContact' does not exist in 'opportunity' object"`;
|
||||
|
||||
exports[`checkFields should reject invalid field name 1`] = `"field 'wrongField' does not exist in 'opportunity' object"`;
|
||||
|
||||
exports[`checkFields should reject mix of valid and invalid field names 1`] = `"field 'wrongField' does not exist in 'opportunity' object"`;
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
|
||||
import { OPPORTUNITY_WITH_FIELDS_MAPS } from 'src/engine/api/rest/core/query-builder/utils/__tests__/mocks/opportunity-field-maps.mock';
|
||||
import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.utils';
|
||||
|
||||
describe('checkFields', () => {
|
||||
const testCases: EachTestingContext<{
|
||||
fields: string[];
|
||||
shouldThrow?: boolean;
|
||||
}>[] = [
|
||||
{
|
||||
title: 'should accept valid join column id',
|
||||
context: {
|
||||
fields: ['pointOfContactId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should accept valid relation field name',
|
||||
context: {
|
||||
shouldThrow: true,
|
||||
fields: ['pointOfContact'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should accept valid field name',
|
||||
context: {
|
||||
fields: ['position'],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should reject invalid field name',
|
||||
context: {
|
||||
fields: ['wrongField'],
|
||||
shouldThrow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should reject mix of valid and invalid field names',
|
||||
context: {
|
||||
fields: ['position', 'wrongField'],
|
||||
shouldThrow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should accept composite field',
|
||||
context: {
|
||||
fields: ['source'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testCases)('$title', ({ context }) => {
|
||||
if (context.shouldThrow) {
|
||||
expect(() =>
|
||||
checkFields(OPPORTUNITY_WITH_FIELDS_MAPS, context.fields),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
} else {
|
||||
expect(() =>
|
||||
checkFields(OPPORTUNITY_WITH_FIELDS_MAPS, context.fields),
|
||||
).not.toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { computeDepth } from 'src/engine/api/rest/core/query-builder/utils/compute-depth.utils';
|
||||
|
||||
describe('computeDepth', () => {
|
||||
[0, 1].forEach((depth) => {
|
||||
it('should compute depth from query', () => {
|
||||
const request: any = {
|
||||
query: { depth: `${depth}` },
|
||||
};
|
||||
|
||||
expect(computeDepth(request)).toEqual(depth);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default depth if missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(computeDepth(request)).toEqual(undefined);
|
||||
});
|
||||
it('should raise if wrong depth', () => {
|
||||
const request: any = { query: { depth: '100' } };
|
||||
|
||||
expect(() => computeDepth(request)).toThrow();
|
||||
|
||||
request.query.depth = '-1';
|
||||
|
||||
expect(() => computeDepth(request)).toThrow();
|
||||
});
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
fieldNumberMock,
|
||||
objectMetadataItemMock,
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { getFieldType } from 'src/engine/api/rest/core/query-builder/utils/get-field-type.utils';
|
||||
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
|
||||
describe('getFieldType', () => {
|
||||
const completeFieldNumberMock = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
id: 'field-number-id',
|
||||
type: fieldNumberMock.type,
|
||||
name: fieldNumberMock.name,
|
||||
label: 'Field Number',
|
||||
isNullable: fieldNumberMock.isNullable,
|
||||
defaultValue: fieldNumberMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const fieldsById: FieldMetadataMap = {
|
||||
'field-number-id': completeFieldNumberMock,
|
||||
};
|
||||
|
||||
const mockObjectMetadataWithFieldMaps = {
|
||||
...objectMetadataItemMock,
|
||||
fieldsById,
|
||||
fieldIdByName: {
|
||||
[completeFieldNumberMock.name]: completeFieldNumberMock.id,
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
indexMetadatas: [],
|
||||
};
|
||||
|
||||
it('should get field type', () => {
|
||||
expect(
|
||||
getFieldType(mockObjectMetadataWithFieldMaps, 'fieldNumber'),
|
||||
).toEqual(FieldMetadataType.NUMBER);
|
||||
});
|
||||
});
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataRelationSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import {
|
||||
fieldCurrencyMock,
|
||||
fieldNumberMock,
|
||||
fieldTextMock,
|
||||
objectMetadataItemMock,
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils';
|
||||
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
|
||||
describe('mapFieldMetadataToGraphqlQuery', () => {
|
||||
const typedFieldNumberMock = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
id: '20202020-0000-0000-0000-000000000002',
|
||||
name: fieldNumberMock.name,
|
||||
type: fieldNumberMock.type,
|
||||
label: 'Field Number',
|
||||
isNullable: fieldNumberMock.isNullable,
|
||||
defaultValue: fieldNumberMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const typedFieldTextMock = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
id: '20202020-0000-0000-0000-000000000003',
|
||||
name: fieldTextMock.name,
|
||||
type: fieldTextMock.type,
|
||||
label: 'Field Text',
|
||||
isNullable: fieldTextMock.isNullable,
|
||||
defaultValue: fieldTextMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const typedFieldCurrencyMock = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
id: '20202020-0000-0000-0000-000000000004',
|
||||
name: fieldCurrencyMock.name,
|
||||
type: fieldCurrencyMock.type,
|
||||
label: 'Field Currency',
|
||||
isNullable: fieldCurrencyMock.isNullable,
|
||||
defaultValue: fieldCurrencyMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const fieldsById: FieldMetadataMap = {
|
||||
'field-number-id': typedFieldNumberMock,
|
||||
'field-text-id': typedFieldTextMock,
|
||||
'field-currency-id': typedFieldCurrencyMock,
|
||||
};
|
||||
|
||||
const typedObjectMetadataItem: ObjectMetadataItemWithFieldMaps = {
|
||||
...objectMetadataItemMock,
|
||||
fieldsById,
|
||||
fieldIdByName: {
|
||||
[typedFieldNumberMock.name]: typedFieldNumberMock.id,
|
||||
[typedFieldTextMock.name]: typedFieldTextMock.id,
|
||||
[typedFieldCurrencyMock.name]: typedFieldCurrencyMock.id,
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
indexMetadatas: [],
|
||||
};
|
||||
|
||||
const objectMetadataMapsMock: ObjectMetadataMaps = {
|
||||
byId: {
|
||||
[objectMetadataItemMock.id]: typedObjectMetadataItem,
|
||||
},
|
||||
idByNameSingular: {
|
||||
[objectMetadataItemMock.nameSingular]: objectMetadataItemMock.id,
|
||||
},
|
||||
};
|
||||
|
||||
it('should map properly', () => {
|
||||
expect(
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadataMapsMock,
|
||||
typedFieldNumberMock,
|
||||
),
|
||||
).toEqual('fieldNumber');
|
||||
expect(
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadataMapsMock,
|
||||
typedFieldTextMock,
|
||||
),
|
||||
).toEqual('fieldText');
|
||||
expect(
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadataMapsMock,
|
||||
typedFieldCurrencyMock,
|
||||
),
|
||||
).toEqual(`
|
||||
fieldCurrency
|
||||
{
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
describe('should handle all field metadata types', () => {
|
||||
Object.values(FieldMetadataType).forEach((fieldMetadataType) => {
|
||||
it(`with field type ${fieldMetadataType}`, () => {
|
||||
const field = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
id: '20202020-0000-0000-0000-000000000005',
|
||||
type: fieldMetadataType,
|
||||
name: 'toObjectMetadataName',
|
||||
label: 'Test Field',
|
||||
isNullable: true,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
settings:
|
||||
fieldMetadataType === FieldMetadataType.RELATION ||
|
||||
fieldMetadataType === FieldMetadataType.MORPH_RELATION
|
||||
? ({
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
} as FieldMetadataRelationSettings)
|
||||
: null,
|
||||
});
|
||||
|
||||
expect(
|
||||
mapFieldMetadataToGraphqlQuery(objectMetadataMapsMock, field),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { DateDisplayFormat } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock';
|
||||
|
||||
const workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419';
|
||||
const objectMetadataId = '20202020-6e2c-42f6-a83c-cc58d776af88';
|
||||
|
||||
export const OPPORTUNITY_WITH_FIELDS_MAPS =
|
||||
getMockObjectMetadataItemWithFieldsMaps({
|
||||
id: objectMetadataId,
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
labelSingular: 'Opportunity',
|
||||
labelPlural: 'Opportunities',
|
||||
description: 'An opportunity',
|
||||
icon: 'IconTargetArrow',
|
||||
targetTableName: 'DEPRECATED',
|
||||
isCustom: false,
|
||||
isRemote: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isAuditLogged: true,
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataId: '20202020-c2f1-4435-adca-22931f8b41b6',
|
||||
imageIdentifierFieldMetadataId: null,
|
||||
workspaceId,
|
||||
indexMetadatas: [],
|
||||
fieldsById: {
|
||||
'20202020-c2f1-4435-adca-22931f8b41b6': getMockFieldMetadataEntity({
|
||||
id: '20202020-c2f1-4435-adca-22931f8b41b6',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: "''",
|
||||
description: 'The opportunity name',
|
||||
icon: 'IconTargetArrow',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-5eef-417a-b517-ebeedaa8e10b': getMockFieldMetadataEntity({
|
||||
id: '20202020-5eef-417a-b517-ebeedaa8e10b',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
name: 'amount',
|
||||
label: 'Amount',
|
||||
defaultValue: { amountMicros: null, currencyCode: "''" },
|
||||
description: 'Opportunity amount',
|
||||
icon: 'IconCurrencyDollar',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-597c-44d3-98ec-ea71aea5256b': getMockFieldMetadataEntity({
|
||||
id: '20202020-597c-44d3-98ec-ea71aea5256b',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'closeDate',
|
||||
label: 'Close date',
|
||||
defaultValue: null,
|
||||
description: 'Opportunity close date',
|
||||
icon: 'IconCalendarEvent',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-9b94-454a-94ca-8afb09c68faf': getMockFieldMetadataEntity({
|
||||
id: '20202020-9b94-454a-94ca-8afb09c68faf',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.SELECT,
|
||||
name: 'stage',
|
||||
label: 'Stage',
|
||||
defaultValue: "'NEW'",
|
||||
description: 'Opportunity stage',
|
||||
icon: 'IconProgressCheck',
|
||||
options: [
|
||||
{
|
||||
id: '20202020-dba8-4975-81bd-b29a41c0a387',
|
||||
color: 'red',
|
||||
label: 'New',
|
||||
value: 'NEW',
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
id: '20202020-1c9d-490c-940c-bf47addcd6a1',
|
||||
color: 'purple',
|
||||
label: 'Screening',
|
||||
value: 'SCREENING',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '20202020-1368-438b-8702-6d5e5727a888',
|
||||
color: 'sky',
|
||||
label: 'Meeting',
|
||||
value: 'MEETING',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '20202020-41e4-4b4e-a038-f02645f55767',
|
||||
color: 'turquoise',
|
||||
label: 'Proposal',
|
||||
value: 'PROPOSAL',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
id: '20202020-8acf-4934-8519-42f7c9133cd5',
|
||||
color: 'yellow',
|
||||
label: 'Customer',
|
||||
value: 'CUSTOMER',
|
||||
position: 4,
|
||||
},
|
||||
],
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-30a5-4d8e-9b93-12d31ece0aaa': getMockFieldMetadataEntity({
|
||||
id: '20202020-30a5-4d8e-9b93-12d31ece0aaa',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.POSITION,
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
defaultValue: 0,
|
||||
description: 'Opportunity record position',
|
||||
icon: 'IconHierarchy2',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-f95f-424f-ab32-65961e8e9635': getMockFieldMetadataEntity({
|
||||
id: '20202020-f95f-424f-ab32-65961e8e9635',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.ACTOR,
|
||||
name: 'createdBy',
|
||||
label: 'Created by',
|
||||
defaultValue: { name: "'System'", source: "'MANUAL'" },
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-5e10-4780-babb-38a465ac546c': getMockFieldMetadataEntity({
|
||||
id: '20202020-5e10-4780-babb-38a465ac546c',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'searchVector',
|
||||
label: 'Search vector',
|
||||
defaultValue: null,
|
||||
description: 'Field used for full-text search',
|
||||
icon: 'IconUser',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-8f4a-4f8d-822e-90fe72f75b79': getMockFieldMetadataEntity({
|
||||
id: '20202020-8f4a-4f8d-822e-90fe72f75b79',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.UUID,
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
defaultValue: 'uuid',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-f120-4b59-b239-f7f1d8eb243e': getMockFieldMetadataEntity({
|
||||
id: '20202020-f120-4b59-b239-f7f1d8eb243e',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
defaultValue: 'now',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
settings: { displayFormat: DateDisplayFormat.RELATIVE },
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: false,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-dcc8-4318-9756-b87377692561': getMockFieldMetadataEntity({
|
||||
id: '20202020-dcc8-4318-9756-b87377692561',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'updatedAt',
|
||||
label: 'Last update',
|
||||
defaultValue: 'now',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
settings: { displayFormat: DateDisplayFormat.RELATIVE },
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: false,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-1694-4f8b-8760-61a5ff330022': getMockFieldMetadataEntity({
|
||||
id: '20202020-1694-4f8b-8760-61a5ff330022',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'deletedAt',
|
||||
label: 'Deletion date',
|
||||
defaultValue: null,
|
||||
description: 'Record deletion date',
|
||||
icon: 'IconCalendarOff',
|
||||
settings: { displayFormat: DateDisplayFormat.RELATIVE },
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: false,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-4f52-4dea-a116-723f9bf7f082': getMockFieldMetadataEntity({
|
||||
id: '20202020-4f52-4dea-a116-723f9bf7f082',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'pointOfContact',
|
||||
label: 'Point of Contact',
|
||||
description: 'The point of contact for this opportunity',
|
||||
icon: 'IconUser',
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
joinColumnName: 'pointOfContactId',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
},
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-fc02-4be2-be1a-e121daf5400d': getMockFieldMetadataEntity({
|
||||
id: '20202020-fc02-4be2-be1a-e121daf5400d',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
description: 'The company this opportunity is associated with',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
joinColumnName: 'companyId',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
},
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-fd9f-48f0-bd5f-5b0fec6a5de4': getMockFieldMetadataEntity({
|
||||
id: '20202020-fd9f-48f0-bd5f-5b0fec6a5de4',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'favorites',
|
||||
label: 'Favorites',
|
||||
description: 'Users who favorited this opportunity',
|
||||
icon: 'IconStar',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
},
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-88ab-4138-98ce-80533bb423e3': getMockFieldMetadataEntity({
|
||||
id: '20202020-88ab-4138-98ce-80533bb423e3',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'taskTargets',
|
||||
label: 'Task Targets',
|
||||
description: 'Tasks targeting this opportunity',
|
||||
icon: 'IconCheckbox',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
},
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-4258-422b-b35b-db3f090af8da': getMockFieldMetadataEntity({
|
||||
id: '20202020-4258-422b-b35b-db3f090af8da',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'noteTargets',
|
||||
label: 'Note Targets',
|
||||
description: 'Notes targeting this opportunity',
|
||||
icon: 'IconNotes',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
},
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-16ca-40a7-a1ba-712975c916cd': getMockFieldMetadataEntity({
|
||||
id: '20202020-16ca-40a7-a1ba-712975c916cd',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'attachments',
|
||||
label: 'Attachments',
|
||||
description: 'Attachments for this opportunity',
|
||||
icon: 'IconPaperclip',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
},
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
'20202020-92a5-47bf-a38d-c1c72b2c3e4d': getMockFieldMetadataEntity({
|
||||
id: '20202020-92a5-47bf-a38d-c1c72b2c3e4d',
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'timelineActivities',
|
||||
label: 'Timeline Activities',
|
||||
description: 'Timeline activities for this opportunity',
|
||||
icon: 'IconTimeline',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
},
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
|
||||
}),
|
||||
},
|
||||
fieldIdByName: {
|
||||
name: '20202020-c2f1-4435-adca-22931f8b41b6',
|
||||
amount: '20202020-5eef-417a-b517-ebeedaa8e10b',
|
||||
closeDate: '20202020-597c-44d3-98ec-ea71aea5256b',
|
||||
stage: '20202020-9b94-454a-94ca-8afb09c68faf',
|
||||
position: '20202020-30a5-4d8e-9b93-12d31ece0aaa',
|
||||
createdBy: '20202020-f95f-424f-ab32-65961e8e9635',
|
||||
searchVector: '20202020-5e10-4780-babb-38a465ac546c',
|
||||
id: '20202020-8f4a-4f8d-822e-90fe72f75b79',
|
||||
createdAt: '20202020-f120-4b59-b239-f7f1d8eb243e',
|
||||
updatedAt: '20202020-dcc8-4318-9756-b87377692561',
|
||||
deletedAt: '20202020-1694-4f8b-8760-61a5ff330022',
|
||||
pointOfContact: '20202020-4f52-4dea-a116-723f9bf7f082',
|
||||
company: '20202020-fc02-4be2-be1a-e121daf5400d',
|
||||
favorites: '20202020-fd9f-48f0-bd5f-5b0fec6a5de4',
|
||||
taskTargets: '20202020-88ab-4138-98ce-80533bb423e3',
|
||||
noteTargets: '20202020-4258-422b-b35b-db3f090af8da',
|
||||
attachments: '20202020-16ca-40a7-a1ba-712975c916cd',
|
||||
timelineActivities: '20202020-92a5-47bf-a38d-c1c72b2c3e4d',
|
||||
},
|
||||
fieldIdByJoinColumnName: {
|
||||
pointOfContactId: '20202020-4f52-4dea-a116-723f9bf7f082',
|
||||
companyId: '20202020-fc02-4be2-be1a-e121daf5400d',
|
||||
},
|
||||
});
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
|
||||
export const checkFields = (
|
||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps,
|
||||
fieldNames: string[],
|
||||
): void => {
|
||||
const fieldMetadataNames: string[] = Object.values(
|
||||
objectMetadataItemWithFieldsMaps.fieldsById,
|
||||
)
|
||||
.flatMap((field) => {
|
||||
if (isCompositeFieldMetadataType(field.type)) {
|
||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new BadRequestException(
|
||||
`Composite type '${field.type}' not found`,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Don't really know why we need to put fieldName and compositeType name here
|
||||
return [
|
||||
field.name,
|
||||
compositeType.properties.map(
|
||||
(compositeProperty) => compositeProperty.name,
|
||||
),
|
||||
].flat();
|
||||
}
|
||||
|
||||
if (isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) {
|
||||
return field.settings?.joinColumnName;
|
||||
}
|
||||
|
||||
return field.name;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
for (const fieldName of fieldNames) {
|
||||
if (!fieldMetadataNames.includes(fieldName)) {
|
||||
throw new BadRequestException(
|
||||
`field '${fieldName}' does not exist in '${computeObjectTargetTable(
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
)}' object`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
|
||||
const ALLOWED_DEPTH_VALUES = [0, 1];
|
||||
|
||||
export const computeDepth = (request: Request): number | undefined => {
|
||||
if (!request.query.depth) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const depth = +request.query.depth;
|
||||
|
||||
if (isNaN(depth) || !ALLOWED_DEPTH_VALUES.includes(depth)) {
|
||||
throw new BadRequestException(
|
||||
`'depth=${
|
||||
request.query.depth
|
||||
}' parameter invalid. Allowed values are ${ALLOWED_DEPTH_VALUES.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return depth;
|
||||
};
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
fieldNumberMock,
|
||||
fieldTextMock,
|
||||
objectMetadataItemMock,
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
||||
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
|
||||
describe('parseFilter', () => {
|
||||
const completeFieldNumberMock = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
id: '20202020-0000-0000-0000-000000000002',
|
||||
type: fieldNumberMock.type,
|
||||
name: fieldNumberMock.name,
|
||||
label: 'Field Number',
|
||||
isNullable: fieldNumberMock.isNullable,
|
||||
defaultValue: fieldNumberMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const completeFieldTextMock = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
id: '20202020-0000-0000-0000-000000000003',
|
||||
type: fieldTextMock.type,
|
||||
name: fieldTextMock.name,
|
||||
label: 'Field Text',
|
||||
isNullable: fieldTextMock.isNullable,
|
||||
defaultValue: fieldTextMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const fieldsById: FieldMetadataMap = {
|
||||
'field-number-id': completeFieldNumberMock,
|
||||
'field-text-id': completeFieldTextMock,
|
||||
};
|
||||
|
||||
const mockObjectMetadataWithFieldMaps: ObjectMetadataItemWithFieldMaps = {
|
||||
...objectMetadataItemMock,
|
||||
fieldsById,
|
||||
fieldIdByName: {
|
||||
[completeFieldNumberMock.name]: completeFieldNumberMock.id,
|
||||
[completeFieldTextMock.name]: completeFieldTextMock.id,
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
indexMetadatas: [],
|
||||
};
|
||||
|
||||
it('should parse string filter test 1', () => {
|
||||
expect(
|
||||
parseFilter(
|
||||
'and(fieldNumber[eq]:1,fieldNumber[eq]:2)',
|
||||
mockObjectMetadataWithFieldMaps,
|
||||
),
|
||||
).toEqual({
|
||||
and: [{ fieldNumber: { eq: '1' } }, { fieldNumber: { eq: '2' } }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse string filter test 2', () => {
|
||||
expect(
|
||||
parseFilter(
|
||||
'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3))',
|
||||
mockObjectMetadataWithFieldMaps,
|
||||
),
|
||||
).toEqual({
|
||||
and: [
|
||||
{ fieldNumber: { eq: '1' } },
|
||||
{ or: [{ fieldNumber: { eq: '2' } }, { fieldNumber: { eq: '3' } }] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse string filter test 3', () => {
|
||||
expect(
|
||||
parseFilter(
|
||||
'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3,and(fieldNumber[eq]:6,fieldNumber[eq]:7)),or(fieldNumber[eq]:4,fieldNumber[eq]:5))',
|
||||
mockObjectMetadataWithFieldMaps,
|
||||
),
|
||||
).toEqual({
|
||||
and: [
|
||||
{ fieldNumber: { eq: '1' } },
|
||||
{
|
||||
or: [
|
||||
{ fieldNumber: { eq: '2' } },
|
||||
{ fieldNumber: { eq: '3' } },
|
||||
{
|
||||
and: [{ fieldNumber: { eq: '6' } }, { fieldNumber: { eq: '7' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ or: [{ fieldNumber: { eq: '4' } }, { fieldNumber: { eq: '5' } }] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse string filter test 4', () => {
|
||||
expect(
|
||||
parseFilter(
|
||||
'and(fieldText[gt]:"val,ue",or(fieldNumber[is]:NOT_NULL,not(fieldText[startsWith]:"val"),and(fieldNumber[eq]:6,fieldText[ilike]:"%val%")),or(fieldNumber[eq]:4,fieldText[is]:NULL))',
|
||||
mockObjectMetadataWithFieldMaps,
|
||||
),
|
||||
).toEqual({
|
||||
and: [
|
||||
{ fieldText: { gt: 'val,ue' } },
|
||||
{
|
||||
or: [
|
||||
{ fieldNumber: { is: 'NOT_NULL' } },
|
||||
{ not: { fieldText: { startsWith: 'val' } } },
|
||||
{
|
||||
and: [
|
||||
{ fieldNumber: { eq: '6' } },
|
||||
{ fieldText: { ilike: '%val%' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ or: [{ fieldNumber: { eq: '4' } }, { fieldText: { is: 'NULL' } }] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle not', () => {
|
||||
expect(
|
||||
parseFilter(
|
||||
'and(fieldNumber[eq]:1,not(fieldNumber[eq]:2))',
|
||||
mockObjectMetadataWithFieldMaps,
|
||||
),
|
||||
).toEqual({
|
||||
and: [
|
||||
{ fieldNumber: { eq: '1' } },
|
||||
{
|
||||
not: { fieldNumber: { eq: '2' } },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.utils';
|
||||
import { getFieldType } from 'src/engine/api/rest/core/query-builder/utils/get-field-type.utils';
|
||||
import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
||||
import { checkFilterEnumValues } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util';
|
||||
import { formatFieldValue } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/format-field-values.util';
|
||||
import { parseBaseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util';
|
||||
import { parseFilterContent } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-content.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export enum Conjunctions {
|
||||
or = 'or',
|
||||
and = 'and',
|
||||
not = 'not',
|
||||
}
|
||||
|
||||
export const parseFilter = (
|
||||
filterQuery: string,
|
||||
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||
): Record<string, FieldValue> => {
|
||||
const result: Record<string, FieldValue> = {};
|
||||
const match = filterQuery.match(
|
||||
`^(${Object.values(Conjunctions).join('|')})\\((.+)\\)$`,
|
||||
);
|
||||
|
||||
if (match) {
|
||||
const conjunction = match?.[1];
|
||||
|
||||
if (!conjunction) {
|
||||
throw new BadRequestException(
|
||||
'Error while matching filter query. Conjunction not found',
|
||||
);
|
||||
}
|
||||
const subResult = parseFilterContent(filterQuery).map((elem) =>
|
||||
parseFilter(elem, objectMetadataItem),
|
||||
);
|
||||
|
||||
if (conjunction === Conjunctions.not) {
|
||||
if (subResult.length > 1) {
|
||||
throw new BadRequestException(
|
||||
`'filter' invalid. 'not' conjunction should contain only 1 condition. eg: not(field[eq]:1)`,
|
||||
);
|
||||
}
|
||||
result[conjunction] = subResult[0];
|
||||
} else {
|
||||
result[conjunction] = subResult;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
const { fields, comparator, value } = parseBaseFilter(filterQuery);
|
||||
|
||||
const fieldName = fields[0];
|
||||
|
||||
checkFields(objectMetadataItem, fields);
|
||||
const fieldType = getFieldType(objectMetadataItem, fieldName);
|
||||
|
||||
checkFilterEnumValues(fieldType, fieldName, value, objectMetadataItem);
|
||||
|
||||
const formattedValue = formatFieldValue(value, fieldType, comparator);
|
||||
|
||||
return fields.reverse().reduce(
|
||||
(acc, currentValue) => {
|
||||
return { [currentValue]: acc };
|
||||
},
|
||||
{ [comparator]: formattedValue },
|
||||
);
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export const getFieldType = (
|
||||
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||
fieldName: string,
|
||||
): FieldMetadataType | undefined => {
|
||||
const fieldMetadataId = objectMetadataItem.fieldIdByName[fieldName];
|
||||
const field = objectMetadataItem.fieldsById[fieldMetadataId];
|
||||
|
||||
return field?.type;
|
||||
};
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
|
||||
const DEFAULT_DEPTH_VALUE = 1;
|
||||
|
||||
// TODO: Should be properly type and based on composite type definitions
|
||||
export const mapFieldMetadataToGraphqlQuery = (
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
field: FieldMetadataEntity,
|
||||
maxDepthForRelations = DEFAULT_DEPTH_VALUE,
|
||||
): string | undefined => {
|
||||
if (maxDepthForRelations < 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const fieldType = field.type;
|
||||
|
||||
const fieldIsSimpleValue = [
|
||||
FieldMetadataType.UUID,
|
||||
FieldMetadataType.TEXT,
|
||||
FieldMetadataType.DATE_TIME,
|
||||
FieldMetadataType.DATE,
|
||||
FieldMetadataType.BOOLEAN,
|
||||
FieldMetadataType.NUMBER,
|
||||
FieldMetadataType.NUMERIC,
|
||||
FieldMetadataType.RATING,
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
FieldMetadataType.POSITION,
|
||||
FieldMetadataType.RAW_JSON,
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
FieldMetadataType.ARRAY,
|
||||
FieldMetadataType.TS_VECTOR,
|
||||
].includes(fieldType);
|
||||
|
||||
const isRelation =
|
||||
isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) ||
|
||||
isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION);
|
||||
|
||||
if (fieldIsSimpleValue) {
|
||||
return field.name;
|
||||
} else if (
|
||||
maxDepthForRelations > 0 &&
|
||||
isRelation &&
|
||||
field.settings?.relationType === RelationType.MANY_TO_ONE
|
||||
) {
|
||||
const targetObjectMetadataId = field.relationTargetObjectMetadataId;
|
||||
|
||||
if (!targetObjectMetadataId) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const relationMetadataItem =
|
||||
objectMetadataMaps.byId[targetObjectMetadataId];
|
||||
|
||||
if (!isDefined(relationMetadataItem)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${field.name}
|
||||
{
|
||||
id
|
||||
${Object.values(relationMetadataItem.fieldsById)
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadataMaps,
|
||||
field,
|
||||
maxDepthForRelations - 1,
|
||||
),
|
||||
)
|
||||
.join('\n')}
|
||||
}`;
|
||||
} else if (
|
||||
maxDepthForRelations > 0 &&
|
||||
isRelation &&
|
||||
field.settings?.relationType === RelationType.ONE_TO_MANY
|
||||
) {
|
||||
const targetObjectMetadataId = field.relationTargetObjectMetadataId;
|
||||
|
||||
if (!targetObjectMetadataId) {
|
||||
return '';
|
||||
}
|
||||
const relationMetadataItem =
|
||||
objectMetadataMaps.byId[targetObjectMetadataId];
|
||||
|
||||
if (!relationMetadataItem) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${field.name}
|
||||
{
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
${Object.values(relationMetadataItem.fieldsById)
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadataMaps,
|
||||
field,
|
||||
maxDepthForRelations - 1,
|
||||
),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
} else if (fieldType === FieldMetadataType.LINKS) {
|
||||
return `
|
||||
${field.name}
|
||||
{
|
||||
primaryLinkLabel
|
||||
primaryLinkUrl
|
||||
secondaryLinks
|
||||
}
|
||||
`;
|
||||
} else if (fieldType === FieldMetadataType.CURRENCY) {
|
||||
return `
|
||||
${field.name}
|
||||
{
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
`;
|
||||
} else if (fieldType === FieldMetadataType.FULL_NAME) {
|
||||
return `
|
||||
${field.name}
|
||||
{
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
`;
|
||||
} else if (fieldType === FieldMetadataType.ADDRESS) {
|
||||
return `
|
||||
${field.name}
|
||||
{
|
||||
addressStreet1
|
||||
addressStreet2
|
||||
addressCity
|
||||
addressPostcode
|
||||
addressState
|
||||
addressCountry
|
||||
addressLat
|
||||
addressLng
|
||||
}
|
||||
`;
|
||||
} else if (fieldType === FieldMetadataType.ACTOR) {
|
||||
return `
|
||||
${field.name}
|
||||
{
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
`;
|
||||
} else if (fieldType === FieldMetadataType.EMAILS) {
|
||||
return `
|
||||
${field.name}
|
||||
{
|
||||
primaryEmail
|
||||
additionalEmails
|
||||
}
|
||||
`;
|
||||
} else if (fieldType === FieldMetadataType.PHONES) {
|
||||
return `
|
||||
${field.name}
|
||||
{
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
`;
|
||||
} else if (fieldType === FieldMetadataType.RICH_TEXT_V2) {
|
||||
return `
|
||||
${field.name}
|
||||
{
|
||||
blocknote
|
||||
markdown
|
||||
}
|
||||
`;
|
||||
}
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { parseCoreBatchPath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-batch-path.utils';
|
||||
|
||||
describe('parseCoreBatchPath', () => {
|
||||
it('should parse object from request path', () => {
|
||||
const request: any = { path: '/rest/batch/companies' };
|
||||
|
||||
expect(parseCoreBatchPath(request)).toEqual({
|
||||
object: 'companies',
|
||||
});
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
|
||||
const testUUID = '20202020-ef5a-4822-9e08-cf6e4a4dcd6b';
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { type Request } from 'express';
|
||||
|
||||
export const parseCoreBatchPath = (request: Request): { object: string } => {
|
||||
return { object: request.path.replace('/rest/batch/', '') };
|
||||
};
|
||||
@@ -18,13 +18,14 @@ import { RestApiRestoreManyHandler } from 'src/engine/api/rest/core/handlers/res
|
||||
import { RestApiRestoreOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-restore-one.handler';
|
||||
import { RestApiUpdateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-many.handler';
|
||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
|
||||
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
||||
import { restToCommonArgsHandlers } from 'src/engine/api/rest/core/rest-to-common-args-handlers/rest-to-common-args-handlers';
|
||||
import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service';
|
||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
@@ -54,6 +55,7 @@ const restApiCoreResolvers = [
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CoreQueryBuilderModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
AuthModule,
|
||||
ApiKeyModule,
|
||||
@@ -66,12 +68,12 @@ const restApiCoreResolvers = [
|
||||
ActorModule,
|
||||
FeatureFlagModule,
|
||||
CoreCommonApiModule,
|
||||
WorkspaceDomainsModule,
|
||||
],
|
||||
controllers: [RestApiCoreController],
|
||||
providers: [
|
||||
RestApiService,
|
||||
RestApiCoreService,
|
||||
...coreQueryBuilderFactories,
|
||||
...restApiCoreResolvers,
|
||||
...restToCommonArgsHandlers,
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -17,9 +17,11 @@ import { RestApiRestoreManyHandler } from 'src/engine/api/rest/core/handlers/res
|
||||
import { RestApiRestoreOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-restore-one.handler';
|
||||
import { RestApiUpdateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-many.handler';
|
||||
import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler';
|
||||
import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { parseSoftDeleteRestRequest } from 'src/engine/api/rest/input-request-parsers/soft-delete-parser-utils/parse-soft-delete-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCoreService {
|
||||
@@ -39,37 +41,83 @@ export class RestApiCoreService {
|
||||
private readonly restApiRestoreOneHandler: RestApiRestoreOneHandler,
|
||||
private readonly restApiRestoreManyHandler: RestApiRestoreManyHandler,
|
||||
private readonly restApiMergeManyHandler: RestApiMergeManyHandler,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
private async isCommonApiEnabled(request: AuthenticatedRequest) {
|
||||
return await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
request.workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
async createOne(request: AuthenticatedRequest) {
|
||||
return await this.restApiCreateOneHandler.handle(request);
|
||||
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
|
||||
|
||||
if (isCommonApiEnabled) {
|
||||
return await this.restApiCreateOneHandler.commonHandle(request);
|
||||
} else {
|
||||
return await this.restApiCreateOneHandler.handle(request);
|
||||
}
|
||||
}
|
||||
|
||||
async createMany(request: AuthenticatedRequest) {
|
||||
return await this.restApiCreateManyHandler.handle(request);
|
||||
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
|
||||
|
||||
if (isCommonApiEnabled) {
|
||||
return await this.restApiCreateManyHandler.commonHandle(request);
|
||||
} else {
|
||||
return await this.restApiCreateManyHandler.handle(request);
|
||||
}
|
||||
}
|
||||
|
||||
async findDuplicates(request: AuthenticatedRequest) {
|
||||
return await this.restApiFindDuplicatesHandler.handle(request);
|
||||
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
|
||||
|
||||
if (isCommonApiEnabled) {
|
||||
return await this.restApiFindDuplicatesHandler.commonHandle(request);
|
||||
} else {
|
||||
return await this.restApiFindDuplicatesHandler.handle(request);
|
||||
}
|
||||
}
|
||||
|
||||
async update(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
|
||||
|
||||
if (isDefined(recordId)) {
|
||||
return await this.restApiUpdateOneHandler.handle(request);
|
||||
if (isCommonApiEnabled) {
|
||||
if (isDefined(recordId)) {
|
||||
return await this.restApiUpdateOneHandler.commonHandle(request);
|
||||
} else {
|
||||
return await this.restApiUpdateManyHandler.handle(request);
|
||||
}
|
||||
} else {
|
||||
return await this.restApiUpdateManyHandler.handle(request);
|
||||
if (isDefined(recordId)) {
|
||||
return await this.restApiUpdateOneHandler.handle(request);
|
||||
} else {
|
||||
throw new BadRequestException(
|
||||
'Activate feature flag to use UpdateMany in the REST API',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async get(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
|
||||
|
||||
if (isDefined(recordId)) {
|
||||
return await this.restApiFindOneHandler.handle(request);
|
||||
if (isCommonApiEnabled) {
|
||||
if (isDefined(recordId)) {
|
||||
return await this.restApiFindOneHandler.commonHandle(request);
|
||||
} else {
|
||||
return await this.restApiFindManyHandler.commonHandle(request);
|
||||
}
|
||||
} else {
|
||||
return await this.restApiFindManyHandler.handle(request);
|
||||
if (isDefined(recordId)) {
|
||||
return await this.restApiFindOneHandler.handle(request);
|
||||
} else {
|
||||
return await this.restApiFindManyHandler.handle(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,30 +128,54 @@ export class RestApiCoreService {
|
||||
async delete(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
|
||||
const isSoftDelete = parseSoftDeleteRestRequest(request);
|
||||
|
||||
if (!isSoftDelete && isDefined(recordId))
|
||||
return await this.restApiDestroyOneHandler.handle(request);
|
||||
if (!isSoftDelete && !isDefined(recordId))
|
||||
if (isCommonApiEnabled && !isSoftDelete && isDefined(recordId))
|
||||
return await this.restApiDestroyOneHandler.commonHandle(request);
|
||||
if (isCommonApiEnabled && !isSoftDelete && !isDefined(recordId))
|
||||
return await this.restApiDestroyManyHandler.handle(request);
|
||||
|
||||
if (isSoftDelete && isDefined(recordId))
|
||||
if (isCommonApiEnabled && isSoftDelete && isDefined(recordId))
|
||||
return await this.restApiDeleteOneHandler.handle(request);
|
||||
if (isSoftDelete && !isDefined(recordId))
|
||||
if (isCommonApiEnabled && isSoftDelete && !isDefined(recordId))
|
||||
return await this.restApiDeleteManyHandler.handle(request);
|
||||
|
||||
if (!isCommonApiEnabled && !isSoftDelete && isDefined(recordId))
|
||||
return await this.restApiDestroyOneHandler.handle(request);
|
||||
|
||||
throw new BadRequestException(
|
||||
'Activate feature flag IS_COMMON_API_ENABLED to use Delete in the REST API',
|
||||
);
|
||||
}
|
||||
|
||||
async restore(request: AuthenticatedRequest) {
|
||||
const { id: recordId } = parseCorePath(request);
|
||||
|
||||
if (isDefined(recordId)) {
|
||||
return await this.restApiRestoreOneHandler.handle(request);
|
||||
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
|
||||
|
||||
if (isCommonApiEnabled) {
|
||||
if (isDefined(recordId)) {
|
||||
return await this.restApiRestoreOneHandler.handle(request);
|
||||
} else {
|
||||
return await this.restApiRestoreManyHandler.handle(request);
|
||||
}
|
||||
} else {
|
||||
return await this.restApiRestoreManyHandler.handle(request);
|
||||
throw new BadRequestException(
|
||||
'Activate feature flag to use Restore in the REST API',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async mergeMany(request: AuthenticatedRequest) {
|
||||
return await this.restApiMergeManyHandler.handle(request);
|
||||
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
|
||||
|
||||
if (isCommonApiEnabled) {
|
||||
return await this.restApiMergeManyHandler.handle(request);
|
||||
} else {
|
||||
throw new BadRequestException(
|
||||
'Activate feature flag to use Merge in the REST API',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import {
|
||||
fieldCurrencyMock,
|
||||
fieldNumberMock,
|
||||
fieldTextMock,
|
||||
objectMetadataMapItemMock,
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
|
||||
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
|
||||
describe('FilterInputFactory', () => {
|
||||
const workspaceId = '20202020-cc80-4306-ad69-da9e11997292';
|
||||
|
||||
const completeFieldNumberMock = getMockFieldMetadataEntity({
|
||||
workspaceId,
|
||||
id: 'field-number-id',
|
||||
type: fieldNumberMock.type,
|
||||
name: fieldNumberMock.name,
|
||||
label: 'Field Number',
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldNumberMock.isNullable,
|
||||
defaultValue: fieldNumberMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const completeFieldTextMock = getMockFieldMetadataEntity({
|
||||
workspaceId,
|
||||
id: 'field-text-id',
|
||||
type: fieldTextMock.type,
|
||||
name: fieldTextMock.name,
|
||||
label: 'Field Text',
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldTextMock.isNullable,
|
||||
defaultValue: fieldTextMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const completeFieldCurrencyMock = getMockFieldMetadataEntity({
|
||||
workspaceId,
|
||||
id: 'field-currency-id',
|
||||
type: fieldCurrencyMock.type,
|
||||
name: fieldCurrencyMock.name,
|
||||
label: 'Field Currency',
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldCurrencyMock.isNullable,
|
||||
defaultValue: fieldCurrencyMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const fieldsById: FieldMetadataMap = {
|
||||
'field-number-id': completeFieldNumberMock,
|
||||
'field-text-id': completeFieldTextMock,
|
||||
'field-currency-id': completeFieldCurrencyMock,
|
||||
};
|
||||
|
||||
const objectMetadataMapItem: ObjectMetadataItemWithFieldMaps = {
|
||||
...objectMetadataMapItemMock,
|
||||
fieldsById,
|
||||
fieldIdByName: {
|
||||
[completeFieldNumberMock.name]: completeFieldNumberMock.id,
|
||||
[completeFieldTextMock.name]: completeFieldTextMock.id,
|
||||
[completeFieldCurrencyMock.name]: completeFieldCurrencyMock.id,
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
};
|
||||
|
||||
const objectMetadataMaps = {
|
||||
byId: {
|
||||
[objectMetadataMapItemMock.id || 'mock-id']: objectMetadataMapItem,
|
||||
},
|
||||
idByNameSingular: {
|
||||
[objectMetadataMapItemMock.nameSingular]:
|
||||
objectMetadataMapItemMock.id || 'mock-id',
|
||||
},
|
||||
};
|
||||
|
||||
const objectMetadata = {
|
||||
objectMetadataMaps,
|
||||
objectMetadataMapItem,
|
||||
};
|
||||
|
||||
let service: FilterInputFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [FilterInputFactory],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FilterInputFactory>(FilterInputFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should return default if filter missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual({});
|
||||
});
|
||||
|
||||
it('should throw when wrong field provided', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
filter: 'wrongField[eq]:1',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => service.create(request, objectMetadata)).toThrow(
|
||||
"field 'wrongField' does not exist in 'objectName' object",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when wrong comparator provided', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
filter: 'fieldNumber[wrongComparator]:1',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => service.create(request, objectMetadata)).toThrow(
|
||||
"'filter' invalid for 'fieldNumber[wrongComparator]:1', comparator wrongComparator not in eq, neq, in, containsAny, is, gt, gte, lt, lte, startsWith, like, ilike",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when wrong filter provided', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
filter: 'fieldNumber[wrongComparator:1',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => service.create(request, objectMetadata)).toThrow(
|
||||
"'filter' invalid for 'fieldNumber[wrongComparator:1'. eg: price[gte]:10",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when parenthesis are not closed', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
filter: 'and(fieldNumber[eq]:1,not(fieldNumber[neq]:1)',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => service.create(request, objectMetadata)).toThrow(
|
||||
"'filter' invalid. 1 close bracket is missing in the query",
|
||||
);
|
||||
});
|
||||
|
||||
it('should create filter parser properly', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
filter: 'fieldNumber[eq]:1,fieldText[eq]:"Test"',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual({
|
||||
and: [{ fieldNumber: { eq: 1 } }, { fieldText: { eq: 'Test' } }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create complex filter parser properly', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
filter:
|
||||
'and(fieldNumber[eq]:1,fieldText[gte]:"Test",not(fieldText[ilike]:"%val%"),or(not(and(fieldText[startsWith]:"test",fieldNumber[in]:[2,4,5])),fieldCurrency.amountMicros[gt]:1))',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual({
|
||||
and: [
|
||||
{ fieldNumber: { eq: 1 } },
|
||||
{ fieldText: { gte: 'Test' } },
|
||||
{ not: { fieldText: { ilike: '%val%' } } },
|
||||
{
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
and: [
|
||||
{ fieldText: { startsWith: 'test' } },
|
||||
{ fieldNumber: { in: [2, 4, 5] } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ fieldCurrency: { amountMicros: { gt: '1' } } },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { OrderByDirection } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
objectMetadataMapItemMock,
|
||||
objectMetadataMapsMock,
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
describe('OrderByInputFactory', () => {
|
||||
const objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
} = {
|
||||
objectMetadataMaps: objectMetadataMapsMock,
|
||||
objectMetadataMapItem: objectMetadataMapItemMock,
|
||||
};
|
||||
|
||||
let service: OrderByInputFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [OrderByInputFactory],
|
||||
}).compile();
|
||||
|
||||
service = module.get<OrderByInputFactory>(OrderByInputFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should return default if order by missing', () => {
|
||||
const request: any = { query: {} };
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual([
|
||||
{},
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should create order by parser properly', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldNumber[AscNullsFirst],fieldText[DescNullsLast]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual([
|
||||
{ fieldNumber: OrderByDirection.AscNullsFirst },
|
||||
{ fieldText: OrderByDirection.DescNullsLast },
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should choose default direction if missing', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldNumber',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual([
|
||||
{ fieldNumber: OrderByDirection.AscNullsFirst },
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle complex fields', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldCurrency.amountMicros',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual([
|
||||
{ fieldCurrency: { amountMicros: OrderByDirection.AscNullsFirst } },
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle complex fields with direction', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldCurrency.amountMicros[DescNullsLast]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual([
|
||||
{ fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast } },
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle multiple complex fields with direction', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by:
|
||||
'fieldCurrency.amountMicros[DescNullsLast],fieldText.label[AscNullsLast]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(service.create(request, objectMetadata)).toEqual([
|
||||
{ fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast } },
|
||||
{ fieldText: { label: OrderByDirection.AscNullsLast } },
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw if direction invalid', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'fieldText[invalid]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => service.create(request, objectMetadata)).toThrow(
|
||||
"'order_by' direction 'invalid' invalid. Allowed values are 'AscNullsFirst', 'AscNullsLast', 'DescNullsFirst', 'DescNullsLast'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if field invalid', () => {
|
||||
const request: any = {
|
||||
query: {
|
||||
order_by: 'wrongField[DescNullsLast]',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => service.create(request, objectMetadata)).toThrow(
|
||||
"field 'wrongField' does not exist in 'objectName' object",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
|
||||
@Injectable()
|
||||
export class EndingBeforeInputFactory {
|
||||
create(request: RequestContext): string | undefined {
|
||||
const cursorQuery = request.query?.ending_before;
|
||||
|
||||
if (typeof cursorQuery !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cursorQuery;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
|
||||
import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
||||
|
||||
export const inputFactories = [FilterInputFactory, OrderByInputFactory];
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
||||
import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
||||
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util';
|
||||
import { checkFilterQuery } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
@Injectable()
|
||||
export class FilterInputFactory {
|
||||
create(
|
||||
request: Request,
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
},
|
||||
): Record<string, FieldValue> {
|
||||
let filterQuery = request.query.filter;
|
||||
|
||||
if (typeof filterQuery !== 'string') {
|
||||
return {};
|
||||
}
|
||||
|
||||
checkFilterQuery(filterQuery);
|
||||
|
||||
filterQuery = addDefaultConjunctionIfMissing(filterQuery);
|
||||
|
||||
return parseFilter(filterQuery, objectMetadata.objectMetadataMapItem);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Request } from 'express';
|
||||
import { OrderByDirection } from 'twenty-shared/types';
|
||||
|
||||
import { ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.utils';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
export const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst;
|
||||
|
||||
@Injectable()
|
||||
export class OrderByInputFactory {
|
||||
create(
|
||||
request: Request,
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
},
|
||||
): ObjectRecordOrderBy {
|
||||
const orderByQuery = request.query.order_by;
|
||||
|
||||
if (typeof orderByQuery !== 'string') {
|
||||
return this.addDefaultOrderById([{}]);
|
||||
}
|
||||
|
||||
//orderByQuery = field_1[AscNullsFirst],field_2[DescNullsLast],field_3
|
||||
const orderByItems = orderByQuery.split(',');
|
||||
let result: Array<Record<string, OrderByDirection>> = [];
|
||||
let itemDirection = '';
|
||||
let itemFields = '';
|
||||
|
||||
for (const orderByItem of orderByItems) {
|
||||
// orderByItem -> field_1[AscNullsFirst]
|
||||
if (orderByItem.includes('[') && orderByItem.includes(']')) {
|
||||
const [fieldsString, direction] = orderByItem
|
||||
.replace(']', '')
|
||||
.split('[');
|
||||
|
||||
// fields -> [field_1] ; direction -> AscNullsFirst
|
||||
if (!(direction in OrderByDirection)) {
|
||||
throw new BadRequestException(
|
||||
`'order_by' direction '${direction}' invalid. Allowed values are '${Object.values(
|
||||
OrderByDirection,
|
||||
).join(
|
||||
"', '",
|
||||
)}'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3`,
|
||||
);
|
||||
}
|
||||
|
||||
itemDirection = direction;
|
||||
itemFields = fieldsString;
|
||||
} else {
|
||||
// orderByItem -> field_3
|
||||
itemDirection = DEFAULT_ORDER_DIRECTION;
|
||||
itemFields = orderByItem;
|
||||
}
|
||||
|
||||
let fieldResult = {};
|
||||
|
||||
itemFields
|
||||
.split('.')
|
||||
.reverse()
|
||||
.forEach((field) => {
|
||||
if (Object.keys(fieldResult).length) {
|
||||
fieldResult = { [field]: fieldResult };
|
||||
} else {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
fieldResult[field] = itemDirection;
|
||||
}
|
||||
}, itemDirection);
|
||||
|
||||
const resultFields = Object.keys(fieldResult).map((key) => ({
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
[key]: fieldResult[key],
|
||||
}));
|
||||
|
||||
result = [...result, ...resultFields];
|
||||
}
|
||||
|
||||
checkFields(
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
result.flatMap((fields) => Object.keys(fields)),
|
||||
);
|
||||
|
||||
return this.addDefaultOrderById(result);
|
||||
}
|
||||
|
||||
addDefaultOrderById(orderBy: ObjectRecordOrderBy) {
|
||||
const hasIdOrder = orderBy.some((o) => Object.keys(o).includes('id'));
|
||||
|
||||
return hasIdOrder
|
||||
? orderBy
|
||||
: [...orderBy, { id: OrderByDirection.AscNullsFirst }];
|
||||
}
|
||||
}
|
||||
+13
-7
@@ -1,15 +1,19 @@
|
||||
import { parseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter.util';
|
||||
import { parseFilterWithoutMetadataValidation } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-without-metadata-validation.util';
|
||||
|
||||
describe('parseFilter', () => {
|
||||
describe('parseFilterWithoutMetadataValidation', () => {
|
||||
it('should parse string filter test 1', () => {
|
||||
expect(parseFilter('and(fieldNumber[eq]:1,fieldNumber[eq]:2)')).toEqual({
|
||||
expect(
|
||||
parseFilterWithoutMetadataValidation(
|
||||
'and(fieldNumber[eq]:1,fieldNumber[eq]:2)',
|
||||
),
|
||||
).toEqual({
|
||||
and: [{ fieldNumber: { eq: '1' } }, { fieldNumber: { eq: '2' } }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse string filter test 2', () => {
|
||||
expect(
|
||||
parseFilter(
|
||||
parseFilterWithoutMetadataValidation(
|
||||
'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3))',
|
||||
),
|
||||
).toEqual({
|
||||
@@ -22,7 +26,7 @@ describe('parseFilter', () => {
|
||||
|
||||
it('should parse string filter test 3', () => {
|
||||
expect(
|
||||
parseFilter(
|
||||
parseFilterWithoutMetadataValidation(
|
||||
'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3,and(fieldNumber[eq]:6,fieldNumber[eq]:7)),or(fieldNumber[eq]:4,fieldNumber[eq]:5))',
|
||||
),
|
||||
).toEqual({
|
||||
@@ -44,7 +48,7 @@ describe('parseFilter', () => {
|
||||
|
||||
it('should parse string filter test 4', () => {
|
||||
expect(
|
||||
parseFilter(
|
||||
parseFilterWithoutMetadataValidation(
|
||||
'and(fieldText[gt]:"val,ue",or(fieldNumber[is]:NOT_NULL,not(fieldText[startsWith]:"val"),and(fieldNumber[eq]:6,fieldText[ilike]:"%val%")),or(fieldNumber[eq]:4,fieldText[is]:NULL))',
|
||||
),
|
||||
).toEqual({
|
||||
@@ -69,7 +73,9 @@ describe('parseFilter', () => {
|
||||
|
||||
it('should handle not', () => {
|
||||
expect(
|
||||
parseFilter('and(fieldNumber[eq]:1,not(fieldNumber[eq]:2))'),
|
||||
parseFilterWithoutMetadataValidation(
|
||||
'and(fieldNumber[eq]:1,not(fieldNumber[eq]:2))',
|
||||
),
|
||||
).toEqual({
|
||||
and: [
|
||||
{ fieldNumber: { eq: '1' } },
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Conjunctions } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter.util';
|
||||
import { Conjunctions } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
||||
|
||||
export const DEFAULT_CONJUNCTION = Conjunctions.and;
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
||||
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util';
|
||||
import { checkFilterQuery } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util';
|
||||
import { parseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter.util';
|
||||
import { parseFilterWithoutMetadataValidation } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-without-metadata-validation.util';
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
export const parseFilterRestRequest = (
|
||||
@@ -18,5 +18,5 @@ export const parseFilterRestRequest = (
|
||||
|
||||
filterQuery = addDefaultConjunctionIfMissing(filterQuery);
|
||||
|
||||
return parseFilter(filterQuery);
|
||||
return parseFilterWithoutMetadataValidation(filterQuery);
|
||||
};
|
||||
|
||||
+4
-8
@@ -1,5 +1,6 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { Conjunctions } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
||||
import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
||||
import { formatFieldValue } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/format-field-values.util';
|
||||
import { parseBaseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util';
|
||||
@@ -9,13 +10,8 @@ import {
|
||||
RestInputRequestParserExceptionCode,
|
||||
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||
|
||||
export enum Conjunctions {
|
||||
or = 'or',
|
||||
and = 'and',
|
||||
not = 'not',
|
||||
}
|
||||
|
||||
export const parseFilter = (
|
||||
//TODO : Refacto-common - Rename after deleting parseFilter
|
||||
export const parseFilterWithoutMetadataValidation = (
|
||||
filterQuery: string,
|
||||
): Record<string, FieldValue> => {
|
||||
const result = {};
|
||||
@@ -32,7 +28,7 @@ export const parseFilter = (
|
||||
);
|
||||
}
|
||||
const subResult = parseFilterContent(filterQuery).map((elem) =>
|
||||
parseFilter(elem),
|
||||
parseFilterWithoutMetadataValidation(elem),
|
||||
);
|
||||
|
||||
if (conjunction === Conjunctions.not) {
|
||||
+1
-2
@@ -4,6 +4,7 @@ import { OrderByDirection } from 'twenty-shared/types';
|
||||
|
||||
import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
||||
import { addDefaultOrderById } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/add-default-order-by-id.util';
|
||||
import {
|
||||
RestInputRequestParserException,
|
||||
@@ -11,8 +12,6 @@ import {
|
||||
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
export const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst;
|
||||
|
||||
export const parseOrderByRestRequest = (
|
||||
request: AuthenticatedRequest,
|
||||
): ObjectRecordOrderBy => {
|
||||
|
||||
+5
-3
@@ -1,9 +1,10 @@
|
||||
import { CreateMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/create-metadata-query.factory';
|
||||
import { DeleteMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/delete-metadata-query.factory';
|
||||
import { FindManyMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/find-many-metadata-query.factory';
|
||||
import { FindOneMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/find-one-metadata-query.factory';
|
||||
import { FindManyMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/find-many-metadata-query.factory';
|
||||
import { GetMetadataVariablesFactory } from 'src/engine/api/rest/metadata/query-builder/factories/get-metadata-variables.factory';
|
||||
import { inputFactories } from 'src/engine/api/rest/input-factories/factories';
|
||||
import { CreateMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/create-metadata-query.factory';
|
||||
import { UpdateMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/update-metadata-query.factory';
|
||||
import { DeleteMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/delete-metadata-query.factory';
|
||||
|
||||
export const metadataQueryBuilderFactories = [
|
||||
FindOneMetadataQueryFactory,
|
||||
@@ -12,4 +13,5 @@ export const metadataQueryBuilderFactories = [
|
||||
DeleteMetadataQueryFactory,
|
||||
UpdateMetadataQueryFactory,
|
||||
GetMetadataVariablesFactory,
|
||||
...inputFactories,
|
||||
];
|
||||
|
||||
+1
@@ -19,5 +19,6 @@ export enum FeatureFlagKey {
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED',
|
||||
IS_COMMON_API_ENABLED = 'IS_COMMON_API_ENABLED',
|
||||
IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED',
|
||||
}
|
||||
|
||||
+2
@@ -142,6 +142,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_PUBLIC_DOMAIN_ENABLED: false,
|
||||
IS_EMAILING_DOMAIN_ENABLED: false,
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED: false,
|
||||
IS_COMMON_API_ENABLED: false,
|
||||
IS_WORKFLOW_RUN_STOPPAGE_ENABLED: false,
|
||||
},
|
||||
eventEmitterService: {
|
||||
@@ -173,6 +174,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_PUBLIC_DOMAIN_ENABLED: false,
|
||||
IS_EMAILING_DOMAIN_ENABLED: false,
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED: false,
|
||||
IS_COMMON_API_ENABLED: false,
|
||||
},
|
||||
permissionsPerRoleId: {},
|
||||
} as WorkspaceDataSource;
|
||||
|
||||
+5
@@ -61,6 +61,11 @@ export const seedFeatureFlags = async (
|
||||
workspaceId: workspaceId,
|
||||
value: workspaceId === SEED_APPLE_WORKSPACE_ID,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_COMMON_API_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_PAGE_LAYOUT_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
+12
-15
@@ -24,8 +24,7 @@ export const failingFilterInputByFieldMetadataType: {
|
||||
gqlErrorMessage: 'is not defined by type',
|
||||
restFilterInput:
|
||||
'manyToOneRelationField[eq]:"6dd71a46-68fe-4420-82b3-0d5b00ad2642"',
|
||||
restErrorMessage:
|
||||
'column apiInputValidationTestObject.manyToOneRelationField does not exist',
|
||||
restErrorMessage: "'manyToOneRelationField' does not exist",
|
||||
},
|
||||
{
|
||||
gqlFilterInput: {
|
||||
@@ -46,8 +45,7 @@ export const failingFilterInputByFieldMetadataType: {
|
||||
gqlErrorMessage: 'is not defined by type',
|
||||
restFilterInput:
|
||||
'oneToManyRelationFieldId[eq]:"6dd71a46-68fe-4420-82b3-0d5b00ad2642"',
|
||||
restErrorMessage:
|
||||
'Field metadata not found for field: oneToManyRelationFieldId',
|
||||
restErrorMessage: "'oneToManyRelationFieldId' does not exist",
|
||||
},
|
||||
// {
|
||||
// gqlFilterInput: {
|
||||
@@ -316,32 +314,31 @@ export const failingFilterInputByFieldMetadataType: {
|
||||
gqlFilterInput: { multiSelectField: { eq: 'not-a-multi-select' } },
|
||||
gqlErrorMessage: 'Value "not-a-multi-select" does not exist ',
|
||||
restFilterInput: 'multiSelectField[eq]:"not-a-multi-select"',
|
||||
restErrorMessage: 'malformed array literal',
|
||||
restErrorMessage: "not available in 'multiSelectField'",
|
||||
},
|
||||
{
|
||||
gqlFilterInput: { multiSelectField: { eq: {} } },
|
||||
gqlErrorMessage: 'cannot represent non-string value: {}.',
|
||||
restFilterInput: 'multiSelectField[eq]:"{}"',
|
||||
restErrorMessage: "not available in 'multiSelectField'",
|
||||
},
|
||||
// TODO - fix this, should throw
|
||||
// {
|
||||
// gqlFilterInput: { multiSelectField: { eq: {} } },
|
||||
// gqlErrorMessage: 'cannot represent non-string value: {}.',
|
||||
// restFilterInput: 'multiSelectField[eq]:"{}"',
|
||||
// restErrorMessage: "not available in 'multiSelectField'",
|
||||
// },
|
||||
{
|
||||
gqlFilterInput: { multiSelectField: { eq: [] } },
|
||||
gqlErrorMessage: 'cannot represent non-string value: [].',
|
||||
restFilterInput: 'multiSelectField[eq]:"[]"',
|
||||
restErrorMessage: 'malformed array literal',
|
||||
restErrorMessage: "not available in 'multiSelectField'",
|
||||
},
|
||||
{
|
||||
gqlFilterInput: { multiSelectField: { eq: true } },
|
||||
gqlErrorMessage: 'cannot represent non-string value: true.',
|
||||
restFilterInput: 'multiSelectField[eq]:"true"',
|
||||
restErrorMessage: 'malformed array literal',
|
||||
restErrorMessage: "not available in 'multiSelectField'",
|
||||
},
|
||||
{
|
||||
gqlFilterInput: { multiSelectField: { eq: 2 } },
|
||||
gqlErrorMessage: 'cannot represent non-string value: 2.',
|
||||
restFilterInput: 'multiSelectField[eq]:2',
|
||||
restErrorMessage: 'malformed array literal',
|
||||
restErrorMessage: "enum value '2' not available in 'multiSelectField' ",
|
||||
},
|
||||
// TODO - ensure it should throw
|
||||
// {
|
||||
|
||||
+1
-3
@@ -220,9 +220,7 @@ describe('Core REST API Create Many endpoint', () => {
|
||||
})
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.messages[0]).toContain(
|
||||
`A duplicate entry was detected`,
|
||||
);
|
||||
expect(res.body.messages[0]).toContain(`Record already exists`);
|
||||
expect(res.body.error).toBe('BadRequestException');
|
||||
});
|
||||
});
|
||||
|
||||
+1
-3
@@ -188,9 +188,7 @@ describe('Core REST API Create One endpoint', () => {
|
||||
})
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.messages[0]).toContain(
|
||||
`A duplicate entry was detected`,
|
||||
);
|
||||
expect(res.body.messages[0]).toContain(`Record already exists`);
|
||||
expect(res.body.error).toBe('BadRequestException');
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user