Create resolvers and controllers for core views (#13624)

Created:
- Services
- Resolvers
- Controllers
- Tests for services
- Integration tests for GraphQL and Rest

Updated the Rest API playground

Added new feature flag `IS_CORE_VIEW_ENABLED`

Updated `viewFilter` `operand` and `view` `type` to be enums rather than
strings and generated migration file.

Closes https://github.com/twentyhq/core-team-issues/issues/1259

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Raphaël Bosi
2025-08-07 18:45:04 +02:00
committed by GitHub
parent caabe8738d
commit 593b064448
134 changed files with 10987 additions and 62 deletions
@@ -42,6 +42,7 @@ import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.mod
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { CoreViewModule } from 'src/engine/core-modules/view/view.module';
import { WebhookModule } from 'src/engine/core-modules/webhook/webhook.module';
import { WorkflowApiModule } from 'src/engine/core-modules/workflow/workflow-api.module';
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
@@ -118,6 +119,7 @@ import { FileModule } from './file/file.module';
SearchModule,
ApiKeyModule,
WebhookModule,
CoreViewModule,
],
exports: [
AuditModule,
@@ -12,6 +12,7 @@ export enum FeatureFlagKey {
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
IS_TWO_FACTOR_AUTHENTICATION_ENABLED = 'IS_TWO_FACTOR_AUTHENTICATION_ENABLED',
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED',
IS_API_KEY_ROLES_ENABLED = 'IS_API_KEY_ROLES_ENABLED',
@@ -196,6 +196,30 @@ export class OpenApiService {
nameSingular: 'apiKey',
namePlural: 'apiKeys',
},
{
nameSingular: 'view',
namePlural: 'views',
},
{
nameSingular: 'viewField',
namePlural: 'viewFields',
},
{
nameSingular: 'viewFilter',
namePlural: 'viewFilters',
},
{
nameSingular: 'viewSort',
namePlural: 'viewSorts',
},
{
nameSingular: 'viewGroup',
namePlural: 'viewGroups',
},
{
nameSingular: 'viewFilterGroup',
namePlural: 'viewFilterGroups',
},
];
schema.paths = metadata.reduce((path, item) => {
@@ -0,0 +1,90 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
UseFilters,
UseGuards,
} from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
import { UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input';
import { ViewField } from 'src/engine/core-modules/view/entities/view-field.entity';
import { ViewFieldRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-field-rest-api-exception.filter';
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewFields')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseFilters(ViewFieldRestApiExceptionFilter)
export class ViewFieldController {
constructor(private readonly viewFieldService: ViewFieldService) {}
@Get()
async findMany(
@AuthWorkspace() workspace: Workspace,
@Query('viewId') viewId?: string,
): Promise<ViewField[]> {
if (viewId) {
return this.viewFieldService.findByViewId(workspace.id, viewId);
}
return this.viewFieldService.findByWorkspaceId(workspace.id);
}
@Get(':id')
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewField | null> {
return this.viewFieldService.findById(id, workspace.id);
}
@Patch(':id')
async update(
@Param('id') id: string,
@Body() input: UpdateViewFieldInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewField> {
const updatedViewField = await this.viewFieldService.update(
id,
workspace.id,
input,
);
return updatedViewField;
}
@Post()
async create(
@Body() input: CreateViewFieldInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewField> {
return this.viewFieldService.create({
...input,
workspaceId: workspace.id,
});
}
@Delete(':id')
async delete(
@Param('id') id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<{ success: boolean }> {
const deletedViewField = await this.viewFieldService.delete(
id,
workspace.id,
);
return { success: isDefined(deletedViewField) };
}
}
@@ -0,0 +1,92 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
UseFilters,
UseGuards,
} from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { CreateViewFilterGroupInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-filter-group.input';
import { UpdateViewFilterGroupInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-filter-group.input';
import { ViewFilterGroupDTO } from 'src/engine/core-modules/view/dtos/view-filter-group.dto';
import { ViewFilterGroupRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-filter-group-rest-api-exception.filter';
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewFilterGroups')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseFilters(ViewFilterGroupRestApiExceptionFilter)
export class ViewFilterGroupController {
constructor(
private readonly viewFilterGroupService: ViewFilterGroupService,
) {}
@Get()
async findMany(
@AuthWorkspace() workspace: Workspace,
@Query('viewId') viewId?: string,
): Promise<ViewFilterGroupDTO[]> {
if (viewId) {
return this.viewFilterGroupService.findByViewId(workspace.id, viewId);
}
return this.viewFilterGroupService.findByWorkspaceId(workspace.id);
}
@Get(':id')
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterGroupDTO | null> {
return this.viewFilterGroupService.findById(id, workspace.id);
}
@Post()
async create(
@Body() input: CreateViewFilterGroupInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterGroupDTO> {
return this.viewFilterGroupService.create({
...input,
workspaceId: workspace.id,
});
}
@Patch(':id')
async update(
@Param('id') id: string,
@Body() input: UpdateViewFilterGroupInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterGroupDTO> {
const updatedViewFilterGroup = await this.viewFilterGroupService.update(
id,
workspace.id,
input,
);
return updatedViewFilterGroup;
}
@Delete(':id')
async delete(
@Param('id') id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<{ success: boolean }> {
const deletedViewFilterGroup = await this.viewFilterGroupService.delete(
id,
workspace.id,
);
return { success: isDefined(deletedViewFilterGroup) };
}
}
@@ -0,0 +1,90 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
UseFilters,
UseGuards,
} from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { CreateViewFilterInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-filter.input';
import { UpdateViewFilterInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-filter.input';
import { ViewFilterDTO } from 'src/engine/core-modules/view/dtos/view-filter.dto';
import { ViewFilterRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-filter-rest-api-exception.filter';
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewFilters')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseFilters(ViewFilterRestApiExceptionFilter)
export class ViewFilterController {
constructor(private readonly viewFilterService: ViewFilterService) {}
@Get()
async findMany(
@AuthWorkspace() workspace: Workspace,
@Query('viewId') viewId?: string,
): Promise<ViewFilterDTO[]> {
if (viewId) {
return this.viewFilterService.findByViewId(workspace.id, viewId);
}
return this.viewFilterService.findByWorkspaceId(workspace.id);
}
@Get(':id')
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterDTO | null> {
return this.viewFilterService.findById(id, workspace.id);
}
@Post()
async create(
@Body() input: CreateViewFilterInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterDTO> {
return this.viewFilterService.create({
...input,
workspaceId: workspace.id,
});
}
@Patch(':id')
async update(
@Param('id') id: string,
@Body() input: UpdateViewFilterInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterDTO> {
const updatedViewFilter = await this.viewFilterService.update(
id,
workspace.id,
input,
);
return updatedViewFilter;
}
@Delete(':id')
async delete(
@Param('id') id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<{ success: boolean }> {
const deletedViewFilter = await this.viewFilterService.delete(
id,
workspace.id,
);
return { success: isDefined(deletedViewFilter) };
}
}
@@ -0,0 +1,90 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
UseFilters,
UseGuards,
} from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { CreateViewGroupInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-group.input';
import { UpdateViewGroupInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-group.input';
import { ViewGroupDTO } from 'src/engine/core-modules/view/dtos/view-group.dto';
import { ViewGroupRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-group-rest-api-exception.filter';
import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewGroups')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseFilters(ViewGroupRestApiExceptionFilter)
export class ViewGroupController {
constructor(private readonly viewGroupService: ViewGroupService) {}
@Get()
async findMany(
@AuthWorkspace() workspace: Workspace,
@Query('viewId') viewId?: string,
): Promise<ViewGroupDTO[]> {
if (viewId) {
return this.viewGroupService.findByViewId(workspace.id, viewId);
}
return this.viewGroupService.findByWorkspaceId(workspace.id);
}
@Get(':id')
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewGroupDTO | null> {
return this.viewGroupService.findById(id, workspace.id);
}
@Post()
async create(
@Body() input: CreateViewGroupInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewGroupDTO> {
return this.viewGroupService.create({
...input,
workspaceId: workspace.id,
});
}
@Patch(':id')
async update(
@Param('id') id: string,
@Body() input: UpdateViewGroupInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewGroupDTO> {
const updatedViewGroup = await this.viewGroupService.update(
id,
workspace.id,
input,
);
return updatedViewGroup;
}
@Delete(':id')
async delete(
@Param('id') id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<{ success: boolean }> {
const deletedViewGroup = await this.viewGroupService.delete(
id,
workspace.id,
);
return { success: isDefined(deletedViewGroup) };
}
}
@@ -0,0 +1,87 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
UseFilters,
UseGuards,
} from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { CreateViewSortInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-sort.input';
import { UpdateViewSortInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-sort.input';
import { ViewSortDTO } from 'src/engine/core-modules/view/dtos/view-sort.dto';
import { ViewSortRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-sort-rest-api-exception.filter';
import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewSorts')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseFilters(ViewSortRestApiExceptionFilter)
export class ViewSortController {
constructor(private readonly viewSortService: ViewSortService) {}
@Get()
async findMany(
@AuthWorkspace() workspace: Workspace,
@Query('viewId') viewId?: string,
): Promise<ViewSortDTO[]> {
if (viewId) {
return this.viewSortService.findByViewId(workspace.id, viewId);
}
return this.viewSortService.findByWorkspaceId(workspace.id);
}
@Get(':id')
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewSortDTO | null> {
return this.viewSortService.findById(id, workspace.id);
}
@Post()
async create(
@Body() input: CreateViewSortInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewSortDTO> {
return this.viewSortService.create({
...input,
workspaceId: workspace.id,
});
}
@Patch(':id')
async update(
@Param('id') id: string,
@Body() input: UpdateViewSortInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewSortDTO> {
const updatedViewSort = await this.viewSortService.update(
id,
workspace.id,
input,
);
return updatedViewSort;
}
@Delete(':id')
async delete(
@Param('id') id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<{ success: boolean }> {
const deletedViewSort = await this.viewSortService.delete(id, workspace.id);
return { success: isDefined(deletedViewSort) };
}
}
@@ -0,0 +1,87 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
UseFilters,
UseGuards,
} from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { CreateViewInput } from 'src/engine/core-modules/view/dtos/inputs/create-view.input';
import { UpdateViewInput } from 'src/engine/core-modules/view/dtos/inputs/update-view.input';
import { ViewDTO } from 'src/engine/core-modules/view/dtos/view.dto';
import { ViewRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-rest-api-exception.filter';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/views')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseFilters(ViewRestApiExceptionFilter)
export class ViewController {
constructor(private readonly viewService: ViewService) {}
@Get()
@UseGuards(WorkspaceAuthGuard)
async findMany(
@AuthWorkspace() workspace: Workspace,
@Query('objectMetadataId') objectMetadataId?: string,
): Promise<ViewDTO[]> {
if (objectMetadataId) {
return this.viewService.findByObjectMetadataId(
workspace.id,
objectMetadataId,
);
}
return this.viewService.findByWorkspaceId(workspace.id);
}
@Get(':id')
async findOne(
@Param('id') id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewDTO | null> {
return this.viewService.findById(id, workspace.id);
}
@Post()
async create(
@Body() input: CreateViewInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewDTO> {
return this.viewService.create({
...input,
workspaceId: workspace.id,
});
}
@Patch(':id')
async update(
@Param('id') id: string,
@Body() input: UpdateViewInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewDTO> {
const updatedView = await this.viewService.update(id, workspace.id, input);
return updatedView;
}
@Delete(':id')
async delete(
@Param('id') id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<{ success: boolean }> {
const deletedView = await this.viewService.delete(id, workspace.id);
return { success: isDefined(deletedView) };
}
}
@@ -0,0 +1,25 @@
import { Field, InputType } from '@nestjs/graphql';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class CreateViewFieldInput {
@Field(() => UUIDScalarType, { nullable: false })
fieldMetadataId: string;
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
@Field({ nullable: true, defaultValue: true })
isVisible?: boolean;
@Field({ nullable: true, defaultValue: 0 })
size?: number;
@Field({ nullable: true, defaultValue: 0 })
position?: number;
@Field(() => AggregateOperations, { nullable: true })
aggregateOperation?: AggregateOperations;
}
@@ -0,0 +1,22 @@
import { Field, InputType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewFilterGroupLogicalOperator } from 'src/engine/core-modules/view/enums/view-filter-group-logical-operator';
@InputType()
export class CreateViewFilterGroupInput {
@Field(() => UUIDScalarType, { nullable: true })
parentViewFilterGroupId?: string;
@Field(() => ViewFilterGroupLogicalOperator, {
nullable: true,
defaultValue: ViewFilterGroupLogicalOperator.NOT,
})
logicalOperator?: ViewFilterGroupLogicalOperator;
@Field({ nullable: true })
positionInViewFilterGroup?: number;
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
}
@@ -0,0 +1,30 @@
import { Field, InputType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { RawJSONScalar } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/raw-json.scalar';
import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand';
import { ViewFilterValue } from 'src/engine/core-modules/view/types/view-filter-value.type';
@InputType()
export class CreateViewFilterInput {
@Field(() => UUIDScalarType, { nullable: false })
fieldMetadataId: string;
@Field({ nullable: true, defaultValue: ViewFilterOperand.CONTAINS })
operand?: ViewFilterOperand;
@Field(() => RawJSONScalar, { nullable: false })
value: ViewFilterValue;
@Field(() => UUIDScalarType, { nullable: true })
viewFilterGroupId?: string;
@Field({ nullable: true })
positionInViewFilterGroup?: number;
@Field({ nullable: true })
subFieldName?: string;
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
}
@@ -0,0 +1,21 @@
import { InputType, Field } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class CreateViewGroupInput {
@Field(() => UUIDScalarType, { nullable: false })
fieldMetadataId: string;
@Field({ nullable: true, defaultValue: true })
isVisible?: boolean;
@Field({ nullable: false })
fieldValue: string;
@Field({ nullable: true, defaultValue: 0 })
position?: number;
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
}
@@ -0,0 +1,19 @@
import { Field, InputType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewSortDirection } from 'src/engine/core-modules/view/enums/view-sort-direction';
@InputType()
export class CreateViewSortInput {
@Field(() => UUIDScalarType, { nullable: false })
fieldMetadataId: string;
@Field(() => ViewSortDirection, {
nullable: true,
defaultValue: ViewSortDirection.ASC,
})
direction?: ViewSortDirection;
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
}
@@ -0,0 +1,45 @@
import { Field, InputType } from '@nestjs/graphql';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in';
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
@InputType()
export class CreateViewInput {
@Field({ nullable: false })
name: string;
@Field(() => UUIDScalarType, { nullable: false })
objectMetadataId: string;
@Field(() => ViewType, { nullable: true, defaultValue: ViewType.TABLE })
type?: ViewType;
@Field({ nullable: true, defaultValue: 'INDEX' })
key?: string;
@Field({ nullable: false })
icon: string;
@Field({ nullable: true, defaultValue: 0 })
position?: number;
@Field({ nullable: true, defaultValue: false })
isCompact?: boolean;
@Field(() => ViewOpenRecordIn, {
nullable: true,
defaultValue: ViewOpenRecordIn.SIDE_PANEL,
})
openRecordIn?: ViewOpenRecordIn;
@Field(() => AggregateOperations, { nullable: true })
kanbanAggregateOperation?: AggregateOperations;
@Field(() => UUIDScalarType, { nullable: true })
kanbanAggregateOperationFieldMetadataId?: string;
@Field({ nullable: true })
anyFieldFilterValue?: string;
}
@@ -0,0 +1,18 @@
import { Field, InputType } from '@nestjs/graphql';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
@InputType()
export class UpdateViewFieldInput {
@Field({ nullable: true })
isVisible?: boolean;
@Field({ nullable: true })
size?: number;
@Field({ nullable: true })
position?: number;
@Field(() => AggregateOperations, { nullable: true })
aggregateOperation?: AggregateOperations;
}
@@ -0,0 +1,8 @@
import { InputType, PartialType } from '@nestjs/graphql';
import { CreateViewFilterGroupInput } from './create-view-filter-group.input';
@InputType()
export class UpdateViewFilterGroupInput extends PartialType(
CreateViewFilterGroupInput,
) {}
@@ -0,0 +1,6 @@
import { InputType, PartialType } from '@nestjs/graphql';
import { CreateViewFilterInput } from './create-view-filter.input';
@InputType()
export class UpdateViewFilterInput extends PartialType(CreateViewFilterInput) {}
@@ -0,0 +1,6 @@
import { InputType, PartialType } from '@nestjs/graphql';
import { CreateViewGroupInput } from './create-view-group.input';
@InputType()
export class UpdateViewGroupInput extends PartialType(CreateViewGroupInput) {}
@@ -0,0 +1,6 @@
import { InputType, PartialType } from '@nestjs/graphql';
import { CreateViewSortInput } from './create-view-sort.input';
@InputType()
export class UpdateViewSortInput extends PartialType(CreateViewSortInput) {}
@@ -0,0 +1,6 @@
import { InputType, PartialType } from '@nestjs/graphql';
import { CreateViewInput } from './create-view.input';
@InputType()
export class UpdateViewInput extends PartialType(CreateViewInput) {}
@@ -0,0 +1,44 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
registerEnumType(AggregateOperations, { name: 'AggregateOperations' });
@ObjectType('CoreViewField')
export class ViewFieldDTO {
@IDField(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType, { nullable: false })
fieldMetadataId: string;
@Field({ nullable: false, defaultValue: true })
isVisible: boolean;
@Field({ nullable: false, defaultValue: 0 })
size: number;
@Field({ nullable: false, defaultValue: 0 })
position: number;
@Field(() => AggregateOperations, { nullable: true })
aggregateOperation?: AggregateOperations | null;
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
@Field(() => UUIDScalarType, { nullable: false })
workspaceId: string;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
@Field(() => Date, { nullable: true })
deletedAt?: Date | null;
}
@@ -0,0 +1,43 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewFilterGroupLogicalOperator } from 'src/engine/core-modules/view/enums/view-filter-group-logical-operator';
registerEnumType(ViewFilterGroupLogicalOperator, {
name: 'ViewFilterGroupLogicalOperator',
});
@ObjectType('CoreViewFilterGroup')
export class ViewFilterGroupDTO {
@IDField(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType, { nullable: true })
parentViewFilterGroupId?: string | null;
@Field(() => ViewFilterGroupLogicalOperator, {
nullable: false,
defaultValue: ViewFilterGroupLogicalOperator.NOT,
})
logicalOperator: ViewFilterGroupLogicalOperator;
@Field(() => Number, { nullable: true })
positionInViewFilterGroup?: number | null;
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
@Field(() => UUIDScalarType, { nullable: false })
workspaceId: string;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
@Field(() => Date, { nullable: true })
deletedAt?: Date | null;
}
@@ -0,0 +1,53 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
RawJSONScalar,
UUIDScalarType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand';
import { ViewFilterValue } from 'src/engine/core-modules/view/types/view-filter-value.type';
registerEnumType(ViewFilterOperand, {
name: 'ViewFilterOperand',
});
@ObjectType('CoreViewFilter')
export class ViewFilterDTO {
@IDField(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType, { nullable: false })
fieldMetadataId: string;
@Field({ nullable: false, defaultValue: ViewFilterOperand.CONTAINS })
operand: ViewFilterOperand;
@Field(() => RawJSONScalar, { nullable: false })
value: ViewFilterValue;
@Field(() => UUIDScalarType, { nullable: true })
viewFilterGroupId?: string | null;
@Field(() => Number, { nullable: true })
positionInViewFilterGroup?: number | null;
@Field(() => String, { nullable: true })
subFieldName?: string | null;
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
@Field(() => UUIDScalarType, { nullable: false })
workspaceId: string;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
@Field(() => Date, { nullable: true })
deletedAt?: Date | null;
}
@@ -0,0 +1,38 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('CoreViewGroup')
export class ViewGroupDTO {
@IDField(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType, { nullable: false })
fieldMetadataId: string;
@Field({ nullable: false, defaultValue: true })
isVisible: boolean;
@Field({ nullable: false })
fieldValue: string;
@Field({ nullable: false, defaultValue: 0 })
position: number;
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
@Field(() => UUIDScalarType, { nullable: false })
workspaceId: string;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
@Field(() => Date, { nullable: true })
deletedAt?: Date | null;
}
@@ -0,0 +1,38 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewSortDirection } from 'src/engine/core-modules/view/enums/view-sort-direction';
registerEnumType(ViewSortDirection, { name: 'ViewSortDirection' });
@ObjectType('CoreViewSort')
export class ViewSortDTO {
@IDField(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType, { nullable: false })
fieldMetadataId: string;
@Field(() => ViewSortDirection, {
nullable: false,
defaultValue: ViewSortDirection.ASC,
})
direction: ViewSortDirection;
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
@Field(() => UUIDScalarType, { nullable: false })
workspaceId: string;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
@Field(() => Date, { nullable: true })
deletedAt?: Date | null;
}
@@ -0,0 +1,65 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in';
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
registerEnumType(ViewOpenRecordIn, { name: 'ViewOpenRecordIn' });
registerEnumType(ViewType, { name: 'ViewType' });
@ObjectType('CoreView')
export class ViewDTO {
@IDField(() => UUIDScalarType)
id: string;
@Field({ nullable: false })
name: string;
@Field(() => UUIDScalarType, { nullable: false })
objectMetadataId: string;
@Field(() => ViewType, { nullable: false, defaultValue: ViewType.TABLE })
type: ViewType;
@Field({ nullable: true, defaultValue: 'INDEX' })
key: string;
@Field({ nullable: false })
icon: string;
@Field({ nullable: false, defaultValue: 0 })
position: number;
@Field({ nullable: false, defaultValue: false })
isCompact: boolean;
@Field(() => ViewOpenRecordIn, {
nullable: false,
defaultValue: ViewOpenRecordIn.SIDE_PANEL,
})
openRecordIn: ViewOpenRecordIn;
@Field(() => AggregateOperations, { nullable: true })
kanbanAggregateOperation?: AggregateOperations | null;
@Field(() => UUIDScalarType, { nullable: true })
kanbanAggregateOperationFieldMetadataId?: string | null;
@Field(() => UUIDScalarType, { nullable: false })
workspaceId: string;
@Field(() => String, { nullable: true })
anyFieldFilterValue?: string | null;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
@Field(() => Date, { nullable: true })
deletedAt?: Date | null;
}
@@ -0,0 +1,82 @@
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { View } from 'src/engine/core-modules/view/entities/view.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Entity({ name: 'viewField', schema: 'core' })
@Index('IDX_VIEW_FIELD_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
@Index(
'IDX_VIEW_FIELD_FIELD_METADATA_ID_VIEW_ID_UNIQUE',
['fieldMetadataId', 'viewId'],
{
unique: true,
where: '"deletedAt" IS NULL',
},
)
export class ViewField {
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
fieldMetadataId: string;
@Column({ nullable: false, default: true })
isVisible: boolean;
@Column({ nullable: false, type: 'int', default: 0 })
size: number;
@Column({ nullable: false, type: 'int', default: 0 })
position: number;
@Column({
type: 'enum',
enumName: 'AggregateOperations',
enum: AggregateOperations,
nullable: true,
default: null,
})
aggregateOperation?: AggregateOperations | null;
@Column({ nullable: false, type: 'uuid' })
viewId: string;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date | null;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
@ManyToOne(() => View, (view) => view.viewFields, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'viewId' })
view: Relation<View>;
}
@@ -0,0 +1,89 @@
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewFilter } from 'src/engine/core-modules/view/entities/view-filter.entity';
import { View } from 'src/engine/core-modules/view/entities/view.entity';
import { ViewFilterGroupLogicalOperator } from 'src/engine/core-modules/view/enums/view-filter-group-logical-operator';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Entity({ name: 'viewFilterGroup', schema: 'core' })
@Index('IDX_VIEW_FILTER_GROUP_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
export class ViewFilterGroup {
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: true, type: 'uuid' })
parentViewFilterGroupId?: string | null;
@Column({
type: 'enum',
enumName: 'ViewFilterGroupLogicalOperator',
enum: ViewFilterGroupLogicalOperator,
nullable: false,
default: ViewFilterGroupLogicalOperator.NOT,
})
logicalOperator: ViewFilterGroupLogicalOperator;
@Column({ nullable: true, type: 'int' })
positionInViewFilterGroup?: number | null;
@Column({ nullable: false, type: 'uuid' })
viewId: string;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date | null;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
@ManyToOne(() => View, (view) => view.viewFilterGroups, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'viewId' })
view: Relation<View>;
@OneToMany(() => ViewFilter, (viewFilter) => viewFilter.viewFilterGroup)
viewFilters: Relation<ViewFilter>[];
@ManyToOne(
() => ViewFilterGroup,
(viewFilterGroup) => viewFilterGroup.childViewFilterGroups,
{
onDelete: 'CASCADE',
},
)
@JoinColumn({ name: 'parentViewFilterGroupId' })
parentViewFilterGroup: Relation<ViewFilterGroup>;
@OneToMany(
() => ViewFilterGroup,
(viewFilterGroup) => viewFilterGroup.parentViewFilterGroup,
)
childViewFilterGroups: Relation<ViewFilterGroup>[];
}
@@ -0,0 +1,90 @@
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewFilterGroup } from 'src/engine/core-modules/view/entities/view-filter-group.entity';
import { View } from 'src/engine/core-modules/view/entities/view.entity';
import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand';
import { ViewFilterValue } from 'src/engine/core-modules/view/types/view-filter-value.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Entity({ name: 'viewFilter', schema: 'core' })
@Index('IDX_VIEW_FILTER_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
@Index('IDX_VIEW_FILTER_FIELD_METADATA_ID', ['fieldMetadataId'])
export class ViewFilter {
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
fieldMetadataId: string;
@Column({
nullable: false,
type: 'enum',
enumName: 'ViewFilterOperand',
enum: ViewFilterOperand,
default: ViewFilterOperand.CONTAINS,
})
operand: ViewFilterOperand;
@Column({ nullable: false, type: 'jsonb' })
value: ViewFilterValue;
@Column({ nullable: true, type: 'uuid' })
viewFilterGroupId?: string | null;
@Column({ nullable: true, type: 'int' })
positionInViewFilterGroup?: number | null;
@Column({ nullable: true, type: 'text', default: null })
subFieldName?: string | null;
@Column({ nullable: false, type: 'uuid' })
viewId: string;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date | null;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
@ManyToOne(() => View, (view) => view.viewFilters, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'viewId' })
view: Relation<View>;
@ManyToOne(
() => ViewFilterGroup,
(viewFilterGroup) => viewFilterGroup.viewFilters,
{
onDelete: 'CASCADE',
},
)
@JoinColumn({ name: 'viewFilterGroupId' })
viewFilterGroup: Relation<ViewFilterGroup>;
}
@@ -0,0 +1,64 @@
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { View } from 'src/engine/core-modules/view/entities/view.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Entity({ name: 'viewGroup', schema: 'core' })
@Index('IDX_VIEW_GROUP_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
export class ViewGroup {
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
fieldMetadataId: string;
@Column({ nullable: false, default: true })
isVisible: boolean;
@Column({ nullable: false, type: 'text' })
fieldValue: string;
@Column({ nullable: false, type: 'int', default: 0 })
position: number;
@Column({ nullable: false, type: 'uuid' })
viewId: string;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date | null;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
@ManyToOne(() => View, (view) => view.viewGroups, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'viewId' })
view: Relation<View>;
}
@@ -0,0 +1,73 @@
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { View } from 'src/engine/core-modules/view/entities/view.entity';
import { ViewSortDirection } from 'src/engine/core-modules/view/enums/view-sort-direction';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Entity({ name: 'viewSort', schema: 'core' })
@Index('IDX_VIEW_SORT_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
@Index(
'IDX_VIEW_SORT_FIELD_METADATA_ID_VIEW_ID_UNIQUE',
['fieldMetadataId', 'viewId'],
{
unique: true,
where: '"deletedAt" IS NULL',
},
)
export class ViewSort {
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
fieldMetadataId: string;
@Column({
nullable: false,
type: 'enum',
enumName: 'ViewSortDirection',
enum: ViewSortDirection,
default: ViewSortDirection.ASC,
})
direction: ViewSortDirection;
@Column({ nullable: false, type: 'uuid' })
viewId: string;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date | null;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
@ManyToOne(() => View, (view) => view.viewSorts, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'viewId' })
view: Relation<View>;
}
@@ -0,0 +1,120 @@
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewField } from 'src/engine/core-modules/view/entities/view-field.entity';
import { ViewFilterGroup } from 'src/engine/core-modules/view/entities/view-filter-group.entity';
import { ViewFilter } from 'src/engine/core-modules/view/entities/view-filter.entity';
import { ViewGroup } from 'src/engine/core-modules/view/entities/view-group.entity';
import { ViewSort } from 'src/engine/core-modules/view/entities/view-sort.entity';
import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in';
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Entity({ name: 'view', schema: 'core' })
@Index('IDX_VIEW_WORKSPACE_ID_OBJECT_METADATA_ID', [
'workspaceId',
'objectMetadataId',
])
export class View {
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'text', default: '' })
name: string;
@Column({ nullable: false, type: 'uuid' })
objectMetadataId: string;
@Column({
type: 'enum',
enum: ViewType,
enumName: 'ViewType',
nullable: false,
default: ViewType.TABLE,
})
type: ViewType;
@Column({ nullable: true, type: 'text', default: 'INDEX' })
key: string;
@Column({ nullable: false, type: 'text' })
icon: string;
@Column({ nullable: false, type: 'int', default: 0 })
position: number;
@Column({ nullable: false, default: false, type: 'boolean' })
isCompact: boolean;
@Column({
type: 'enum',
enumName: 'ViewOpenRecordIn',
enum: ViewOpenRecordIn,
nullable: false,
default: ViewOpenRecordIn.SIDE_PANEL,
})
openRecordIn: ViewOpenRecordIn;
@Column({
type: 'enum',
enumName: 'KanbanAggregateOperations',
enum: AggregateOperations,
nullable: true,
default: null,
})
kanbanAggregateOperation?: AggregateOperations | null;
@Column({ nullable: true, type: 'uuid' })
kanbanAggregateOperationFieldMetadataId?: string | null;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date | null;
@Column({ nullable: true, type: 'text', default: null })
anyFieldFilterValue?: string | null;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
@OneToMany(() => ViewField, (viewField) => viewField.view)
viewFields: Relation<ViewField[]>;
@OneToMany(() => ViewFilter, (viewFilter) => viewFilter.view)
viewFilters: Relation<ViewFilter[]>;
@OneToMany(() => ViewSort, (viewSort) => viewSort.view)
viewSorts: Relation<ViewSort[]>;
@OneToMany(() => ViewGroup, (viewGroup) => viewGroup.view)
viewGroups: Relation<ViewGroup[]>;
@OneToMany(() => ViewFilterGroup, (viewFilterGroup) => viewFilterGroup.view)
viewFilterGroups: Relation<ViewFilterGroup[]>;
}
@@ -0,0 +1,5 @@
export enum ViewFilterGroupLogicalOperator {
AND = 'AND',
OR = 'OR',
NOT = 'NOT',
}
@@ -0,0 +1,18 @@
export enum ViewFilterOperand {
IS = 'IS',
IS_NOT_NULL = 'IS_NOT_NULL',
IS_NOT = 'IS_NOT',
LESS_THAN_OR_EQUAL = 'LESS_THAN_OR_EQUAL',
GREATER_THAN_OR_EQUAL = 'GREATER_THAN_OR_EQUAL',
IS_BEFORE = 'IS_BEFORE',
IS_AFTER = 'IS_AFTER',
CONTAINS = 'CONTAINS',
DOES_NOT_CONTAIN = 'DOES_NOT_CONTAIN',
IS_EMPTY = 'IS_EMPTY',
IS_NOT_EMPTY = 'IS_NOT_EMPTY',
IS_RELATIVE = 'IS_RELATIVE',
IS_IN_PAST = 'IS_IN_PAST',
IS_IN_FUTURE = 'IS_IN_FUTURE',
IS_TODAY = 'IS_TODAY',
VECTOR_SEARCH = 'VECTOR_SEARCH',
}
@@ -0,0 +1,4 @@
export enum ViewOpenRecordIn {
SIDE_PANEL = 'SIDE_PANEL',
RECORD_PAGE = 'RECORD_PAGE',
}
@@ -0,0 +1,4 @@
export enum ViewSortDirection {
ASC = 'ASC',
DESC = 'DESC',
}
@@ -0,0 +1,4 @@
export enum ViewType {
TABLE = 'TABLE',
KANBAN = 'KANBAN',
}
@@ -0,0 +1,61 @@
import { t } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export class ViewFieldException extends CustomException {
declare code: ViewFieldExceptionCode;
constructor(
message: string,
code: ViewFieldExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
) {
super(message, code, { userFriendlyMessage });
}
}
export enum ViewFieldExceptionCode {
VIEW_FIELD_NOT_FOUND = 'VIEW_FIELD_NOT_FOUND',
INVALID_VIEW_FIELD_DATA = 'INVALID_VIEW_FIELD_DATA',
}
export enum ViewFieldExceptionMessageKey {
WORKSPACE_ID_REQUIRED = 'WORKSPACE_ID_REQUIRED',
VIEW_ID_REQUIRED = 'VIEW_ID_REQUIRED',
VIEW_FIELD_NOT_FOUND = 'VIEW_FIELD_NOT_FOUND',
INVALID_VIEW_FIELD_DATA = 'INVALID_VIEW_FIELD_DATA',
FIELD_METADATA_ID_REQUIRED = 'FIELD_METADATA_ID_REQUIRED',
}
export const generateViewFieldExceptionMessage = (
key: ViewFieldExceptionMessageKey,
id?: string,
) => {
switch (key) {
case ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return 'WorkspaceId is required';
case ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED:
return 'ViewId is required';
case ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND:
return `View field${id ? ` (id: ${id})` : ''} not found`;
case ViewFieldExceptionMessageKey.INVALID_VIEW_FIELD_DATA:
return `Invalid view field data${id ? ` for view field id: ${id}` : ''}`;
case ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return 'FieldMetadataId is required';
default:
assertUnreachable(key);
}
};
export const generateViewFieldUserFriendlyExceptionMessage = (
key: ViewFieldExceptionMessageKey,
) => {
switch (key) {
case ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return t`WorkspaceId is required to create a view field.`;
case ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED:
return t`ViewId is required to create a view field.`;
case ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return t`FieldMetadataId is required to create a view field.`;
}
};
@@ -0,0 +1,61 @@
import { t } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export class ViewFilterGroupException extends CustomException {
declare code: ViewFilterGroupExceptionCode;
constructor(
message: string,
code: ViewFilterGroupExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
) {
super(message, code, { userFriendlyMessage });
}
}
export enum ViewFilterGroupExceptionCode {
VIEW_FILTER_GROUP_NOT_FOUND = 'VIEW_FILTER_GROUP_NOT_FOUND',
INVALID_VIEW_FILTER_GROUP_DATA = 'INVALID_VIEW_FILTER_GROUP_DATA',
}
export enum ViewFilterGroupExceptionMessageKey {
WORKSPACE_ID_REQUIRED = 'WORKSPACE_ID_REQUIRED',
VIEW_ID_REQUIRED = 'VIEW_ID_REQUIRED',
VIEW_FILTER_GROUP_NOT_FOUND = 'VIEW_FILTER_GROUP_NOT_FOUND',
INVALID_VIEW_FILTER_GROUP_DATA = 'INVALID_VIEW_FILTER_GROUP_DATA',
FIELD_METADATA_ID_REQUIRED = 'FIELD_METADATA_ID_REQUIRED',
}
export const generateViewFilterGroupExceptionMessage = (
key: ViewFilterGroupExceptionMessageKey,
id?: string,
) => {
switch (key) {
case ViewFilterGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return 'WorkspaceId is required';
case ViewFilterGroupExceptionMessageKey.VIEW_ID_REQUIRED:
return 'ViewId is required';
case ViewFilterGroupExceptionMessageKey.VIEW_FILTER_GROUP_NOT_FOUND:
return `View filter group${id ? ` (id: ${id})` : ''} not found`;
case ViewFilterGroupExceptionMessageKey.INVALID_VIEW_FILTER_GROUP_DATA:
return `Invalid view filter group data${id ? ` for view filter group id: ${id}` : ''}`;
case ViewFilterGroupExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return 'FieldMetadataId is required';
default:
assertUnreachable(key);
}
};
export const generateViewFilterGroupUserFriendlyExceptionMessage = (
key: ViewFilterGroupExceptionMessageKey,
) => {
switch (key) {
case ViewFilterGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return t`WorkspaceId is required to create a view filter group.`;
case ViewFilterGroupExceptionMessageKey.VIEW_ID_REQUIRED:
return t`ViewId is required to create a view filter group.`;
case ViewFilterGroupExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return t`FieldMetadataId is required to create a view filter group.`;
}
};
@@ -0,0 +1,61 @@
import { t } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export class ViewFilterException extends CustomException {
declare code: ViewFilterExceptionCode;
constructor(
message: string,
code: ViewFilterExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
) {
super(message, code, { userFriendlyMessage });
}
}
export enum ViewFilterExceptionCode {
VIEW_FILTER_NOT_FOUND = 'VIEW_FILTER_NOT_FOUND',
INVALID_VIEW_FILTER_DATA = 'INVALID_VIEW_FILTER_DATA',
}
export enum ViewFilterExceptionMessageKey {
WORKSPACE_ID_REQUIRED = 'WORKSPACE_ID_REQUIRED',
VIEW_ID_REQUIRED = 'VIEW_ID_REQUIRED',
VIEW_FILTER_NOT_FOUND = 'VIEW_FILTER_NOT_FOUND',
INVALID_VIEW_FILTER_DATA = 'INVALID_VIEW_FILTER_DATA',
FIELD_METADATA_ID_REQUIRED = 'FIELD_METADATA_ID_REQUIRED',
}
export const generateViewFilterExceptionMessage = (
key: ViewFilterExceptionMessageKey,
id?: string,
) => {
switch (key) {
case ViewFilterExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return 'WorkspaceId is required';
case ViewFilterExceptionMessageKey.VIEW_ID_REQUIRED:
return 'ViewId is required';
case ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND:
return `View filter${id ? ` (id: ${id})` : ''} not found`;
case ViewFilterExceptionMessageKey.INVALID_VIEW_FILTER_DATA:
return `Invalid view filter data${id ? ` for view filter id: ${id}` : ''}`;
case ViewFilterExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return 'FieldMetadataId is required';
default:
assertUnreachable(key);
}
};
export const generateViewFilterUserFriendlyExceptionMessage = (
key: ViewFilterExceptionMessageKey,
) => {
switch (key) {
case ViewFilterExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return t`WorkspaceId is required to create a view filter.`;
case ViewFilterExceptionMessageKey.VIEW_ID_REQUIRED:
return t`ViewId is required to create a view filter.`;
case ViewFilterExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return t`FieldMetadataId is required to create a view filter.`;
}
};
@@ -0,0 +1,61 @@
import { t } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export class ViewGroupException extends CustomException {
declare code: ViewGroupExceptionCode;
constructor(
message: string,
code: ViewGroupExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
) {
super(message, code, { userFriendlyMessage });
}
}
export enum ViewGroupExceptionCode {
VIEW_GROUP_NOT_FOUND = 'VIEW_GROUP_NOT_FOUND',
INVALID_VIEW_GROUP_DATA = 'INVALID_VIEW_GROUP_DATA',
}
export enum ViewGroupExceptionMessageKey {
WORKSPACE_ID_REQUIRED = 'WORKSPACE_ID_REQUIRED',
VIEW_ID_REQUIRED = 'VIEW_ID_REQUIRED',
VIEW_GROUP_NOT_FOUND = 'VIEW_GROUP_NOT_FOUND',
INVALID_VIEW_GROUP_DATA = 'INVALID_VIEW_GROUP_DATA',
FIELD_METADATA_ID_REQUIRED = 'FIELD_METADATA_ID_REQUIRED',
}
export const generateViewGroupExceptionMessage = (
key: ViewGroupExceptionMessageKey,
id?: string,
) => {
switch (key) {
case ViewGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return 'WorkspaceId is required';
case ViewGroupExceptionMessageKey.VIEW_ID_REQUIRED:
return 'ViewId is required';
case ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND:
return `View group${id ? ` (id: ${id})` : ''} not found`;
case ViewGroupExceptionMessageKey.INVALID_VIEW_GROUP_DATA:
return `Invalid view group data${id ? ` for view group id: ${id}` : ''}`;
case ViewGroupExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return 'FieldMetadataId is required';
default:
assertUnreachable(key);
}
};
export const generateViewGroupUserFriendlyExceptionMessage = (
key: ViewGroupExceptionMessageKey,
) => {
switch (key) {
case ViewGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return t`WorkspaceId is required to create a view group.`;
case ViewGroupExceptionMessageKey.VIEW_ID_REQUIRED:
return t`ViewId is required to create a view group.`;
case ViewGroupExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return t`FieldMetadataId is required to create a view group.`;
}
};
@@ -0,0 +1,70 @@
import { t } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export class ViewSortException extends CustomException {
declare code: ViewSortExceptionCode;
constructor(
message: string,
code: ViewSortExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
) {
super(message, code, { userFriendlyMessage });
}
}
export enum ViewSortExceptionCode {
VIEW_SORT_NOT_FOUND = 'VIEW_SORT_NOT_FOUND',
INVALID_VIEW_SORT_DATA = 'INVALID_VIEW_SORT_DATA',
}
export enum ViewSortExceptionMessageKey {
WORKSPACE_ID_REQUIRED = 'WORKSPACE_ID_REQUIRED',
VIEW_ID_REQUIRED = 'VIEW_ID_REQUIRED',
VIEW_SORT_NOT_FOUND = 'VIEW_SORT_NOT_FOUND',
INVALID_VIEW_SORT_DATA = 'INVALID_VIEW_SORT_DATA',
FIELD_METADATA_ID_REQUIRED = 'FIELD_METADATA_ID_REQUIRED',
}
export const generateViewSortExceptionMessage = (
key: ViewSortExceptionMessageKey,
id?: string,
) => {
let message = '';
switch (key) {
case ViewSortExceptionMessageKey.WORKSPACE_ID_REQUIRED:
message = `WorkspaceId is required`;
break;
case ViewSortExceptionMessageKey.VIEW_ID_REQUIRED:
message = `ViewId is required`;
break;
case ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND:
message = `View sort${id ? ` (id: ${id})` : ''} not found`;
break;
case ViewSortExceptionMessageKey.INVALID_VIEW_SORT_DATA:
message = `Invalid view sort data${id ? ` for view sort id: ${id}` : ''}`;
break;
case ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
message = `FieldMetadataId is required`;
break;
default:
assertUnreachable(key);
}
return t`${message}`;
};
export const generateViewSortUserFriendlyExceptionMessage = (
key: ViewSortExceptionMessageKey,
) => {
switch (key) {
case ViewSortExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return t`WorkspaceId is required to create a view sort.`;
case ViewSortExceptionMessageKey.VIEW_ID_REQUIRED:
return t`ViewId is required to create a view sort.`;
case ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return t`FieldMetadataId is required to create a view sort.`;
}
};
@@ -0,0 +1,56 @@
import { t } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export class ViewException extends CustomException {
declare code: ViewExceptionCode;
constructor(
message: string,
code: ViewExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
) {
super(message, code, { userFriendlyMessage });
}
}
export enum ViewExceptionCode {
VIEW_NOT_FOUND = 'VIEW_NOT_FOUND',
INVALID_VIEW_DATA = 'INVALID_VIEW_DATA',
}
export enum ViewExceptionMessageKey {
WORKSPACE_ID_REQUIRED = 'WORKSPACE_ID_REQUIRED',
OBJECT_METADATA_ID_REQUIRED = 'OBJECT_METADATA_ID_REQUIRED',
VIEW_NOT_FOUND = 'VIEW_NOT_FOUND',
INVALID_VIEW_DATA = 'INVALID_VIEW_DATA',
}
export const generateViewExceptionMessage = (
key: ViewExceptionMessageKey,
id?: string,
) => {
switch (key) {
case ViewExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return 'WorkspaceId is required';
case ViewExceptionMessageKey.OBJECT_METADATA_ID_REQUIRED:
return 'ObjectMetadataId is required';
case ViewExceptionMessageKey.VIEW_NOT_FOUND:
return `View${id ? ` (id: ${id})` : ''} not found`;
case ViewExceptionMessageKey.INVALID_VIEW_DATA:
return `Invalid view data${id ? ` for view id: ${id}` : ''}`;
default:
assertUnreachable(key);
}
};
export const generateViewUserFriendlyExceptionMessage = (
key: ViewExceptionMessageKey,
) => {
switch (key) {
case ViewExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return t`WorkspaceId is required to create a view.`;
case ViewExceptionMessageKey.OBJECT_METADATA_ID_REQUIRED:
return t`ObjectMetadataId is required to create a view.`;
}
};
@@ -0,0 +1,44 @@
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { Response } from 'express';
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
import {
ViewFieldException,
ViewFieldExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-field.exception';
import { CustomException } from 'src/utils/custom-exception';
@Catch(ViewFieldException)
export class ViewFieldRestApiExceptionFilter implements ExceptionFilter {
constructor(
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
) {}
catch(exception: ViewFieldException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
switch (exception.code) {
case ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
404,
);
case ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
400,
);
default:
// TODO: change to 500 when we have input validation
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
400,
);
}
}
}
@@ -0,0 +1,44 @@
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { Response } from 'express';
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
import {
ViewFilterGroupException,
ViewFilterGroupExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-filter-group.exception';
import { CustomException } from 'src/utils/custom-exception';
@Catch(ViewFilterGroupException)
export class ViewFilterGroupRestApiExceptionFilter implements ExceptionFilter {
constructor(
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
) {}
catch(exception: ViewFilterGroupException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
switch (exception.code) {
case ViewFilterGroupExceptionCode.VIEW_FILTER_GROUP_NOT_FOUND:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
404,
);
case ViewFilterGroupExceptionCode.INVALID_VIEW_FILTER_GROUP_DATA:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
400,
);
default:
// TODO: change to 500 when we have input validation
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
400,
);
}
}
}
@@ -0,0 +1,44 @@
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { Response } from 'express';
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
import {
ViewFilterException,
ViewFilterExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-filter.exception';
import { CustomException } from 'src/utils/custom-exception';
@Catch(ViewFilterException)
export class ViewFilterRestApiExceptionFilter implements ExceptionFilter {
constructor(
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
) {}
catch(exception: ViewFilterException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
switch (exception.code) {
case ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
404,
);
case ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
400,
);
default:
// TODO: change to 500 when we have input validation
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
400,
);
}
}
}
@@ -0,0 +1,44 @@
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { Response } from 'express';
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
import {
ViewGroupException,
ViewGroupExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-group.exception';
import { CustomException } from 'src/utils/custom-exception';
@Catch(ViewGroupException)
export class ViewGroupRestApiExceptionFilter implements ExceptionFilter {
constructor(
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
) {}
catch(exception: ViewGroupException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
switch (exception.code) {
case ViewGroupExceptionCode.VIEW_GROUP_NOT_FOUND:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
404,
);
case ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
400,
);
default:
// TODO: change to 500 when we have input validation
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
400,
);
}
}
}
@@ -0,0 +1,44 @@
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { Response } from 'express';
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
import {
ViewException,
ViewExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view.exception';
import { CustomException } from 'src/utils/custom-exception';
@Catch(ViewException)
export class ViewRestApiExceptionFilter implements ExceptionFilter {
constructor(
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
) {}
catch(exception: ViewException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
switch (exception.code) {
case ViewExceptionCode.VIEW_NOT_FOUND:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
404,
);
case ViewExceptionCode.INVALID_VIEW_DATA:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
400,
);
default:
// TODO: change to 500 when we have input validation
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
400,
);
}
}
}
@@ -0,0 +1,44 @@
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { Response } from 'express';
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
import {
ViewSortException,
ViewSortExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-sort.exception';
import { CustomException } from 'src/utils/custom-exception';
@Catch(ViewSortException)
export class ViewSortRestApiExceptionFilter implements ExceptionFilter {
constructor(
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
) {}
catch(exception: ViewSortException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
switch (exception.code) {
case ViewSortExceptionCode.VIEW_SORT_NOT_FOUND:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
404,
);
case ViewSortExceptionCode.INVALID_VIEW_SORT_DATA:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
400,
);
default:
// TODO: change to 500 when we have input validation
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
400,
);
}
}
}
@@ -0,0 +1,70 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { isDefined } from 'twenty-shared/utils';
import { CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
import { UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input';
import { ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto';
import { ViewField } from 'src/engine/core-modules/view/entities/view-field.entity';
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/core-modules/view/utils/view-graphql-api-exception.filter';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Resolver(() => ViewFieldDTO)
@UseFilters(ViewGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ViewFieldResolver {
constructor(private readonly viewFieldService: ViewFieldService) {}
@Query(() => [ViewFieldDTO])
async getCoreViewFields(
@Args('viewId', { type: () => String }) viewId: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewField[]> {
return this.viewFieldService.findByViewId(workspace.id, viewId);
}
@Query(() => ViewFieldDTO, { nullable: true })
async getCoreViewField(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewField | null> {
return this.viewFieldService.findById(id, workspace.id);
}
@Mutation(() => ViewFieldDTO)
async updateCoreViewField(
@Args('id', { type: () => String }) id: string,
@Args('input') input: UpdateViewFieldInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewField> {
return this.viewFieldService.update(id, workspace.id, input);
}
@Mutation(() => ViewFieldDTO)
async createCoreViewField(
@Args('input') input: CreateViewFieldInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewField> {
return this.viewFieldService.create({
...input,
workspaceId: workspace.id,
});
}
@Mutation(() => Boolean)
async deleteCoreViewField(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<boolean> {
const deletedViewField = await this.viewFieldService.delete(
id,
workspace.id,
);
return isDefined(deletedViewField);
}
}
@@ -0,0 +1,76 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { isDefined } from 'twenty-shared/utils';
import { CreateViewFilterGroupInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-filter-group.input';
import { UpdateViewFilterGroupInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-filter-group.input';
import { ViewFilterGroupDTO } from 'src/engine/core-modules/view/dtos/view-filter-group.dto';
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/core-modules/view/utils/view-graphql-api-exception.filter';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Resolver(() => ViewFilterGroupDTO)
@UseFilters(ViewGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ViewFilterGroupResolver {
constructor(
private readonly viewFilterGroupService: ViewFilterGroupService,
) {}
@Query(() => [ViewFilterGroupDTO])
async getCoreViewFilterGroups(
@AuthWorkspace() workspace: Workspace,
@Args('viewId', { type: () => String, nullable: true })
viewId?: string,
): Promise<ViewFilterGroupDTO[]> {
if (viewId) {
return this.viewFilterGroupService.findByViewId(workspace.id, viewId);
}
return this.viewFilterGroupService.findByWorkspaceId(workspace.id);
}
@Query(() => ViewFilterGroupDTO, { nullable: true })
async getCoreViewFilterGroup(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterGroupDTO | null> {
return this.viewFilterGroupService.findById(id, workspace.id);
}
@Mutation(() => ViewFilterGroupDTO)
async createCoreViewFilterGroup(
@Args('input') input: CreateViewFilterGroupInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterGroupDTO> {
return this.viewFilterGroupService.create({
...input,
workspaceId: workspace.id,
});
}
@Mutation(() => ViewFilterGroupDTO)
async updateCoreViewFilterGroup(
@Args('id', { type: () => String }) id: string,
@Args('input') input: UpdateViewFilterGroupInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterGroupDTO> {
return this.viewFilterGroupService.update(id, workspace.id, input);
}
@Mutation(() => Boolean)
async deleteCoreViewFilterGroup(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<boolean> {
const deletedViewFilterGroup = await this.viewFilterGroupService.delete(
id,
workspace.id,
);
return isDefined(deletedViewFilterGroup);
}
}
@@ -0,0 +1,74 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { isDefined } from 'twenty-shared/utils';
import { CreateViewFilterInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-filter.input';
import { UpdateViewFilterInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-filter.input';
import { ViewFilterDTO } from 'src/engine/core-modules/view/dtos/view-filter.dto';
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/core-modules/view/utils/view-graphql-api-exception.filter';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Resolver(() => ViewFilterDTO)
@UseFilters(ViewGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ViewFilterResolver {
constructor(private readonly viewFilterService: ViewFilterService) {}
@Query(() => [ViewFilterDTO])
async getCoreViewFilters(
@AuthWorkspace() workspace: Workspace,
@Args('viewId', { type: () => String, nullable: true })
viewId?: string,
): Promise<ViewFilterDTO[]> {
if (viewId) {
return this.viewFilterService.findByViewId(workspace.id, viewId);
}
return this.viewFilterService.findByWorkspaceId(workspace.id);
}
@Query(() => ViewFilterDTO, { nullable: true })
async getCoreViewFilter(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterDTO | null> {
return this.viewFilterService.findById(id, workspace.id);
}
@Mutation(() => ViewFilterDTO)
async createCoreViewFilter(
@Args('input') input: CreateViewFilterInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterDTO> {
return this.viewFilterService.create({
...input,
workspaceId: workspace.id,
});
}
@Mutation(() => ViewFilterDTO)
async updateCoreViewFilter(
@Args('id', { type: () => String }) id: string,
@Args('input') input: UpdateViewFilterInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewFilterDTO> {
return this.viewFilterService.update(id, workspace.id, input);
}
@Mutation(() => Boolean)
async deleteCoreViewFilter(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<boolean> {
const deletedViewFilter = await this.viewFilterService.delete(
id,
workspace.id,
);
return isDefined(deletedViewFilter);
}
}
@@ -0,0 +1,74 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { isDefined } from 'twenty-shared/utils';
import { CreateViewGroupInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-group.input';
import { UpdateViewGroupInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-group.input';
import { ViewGroupDTO } from 'src/engine/core-modules/view/dtos/view-group.dto';
import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/core-modules/view/utils/view-graphql-api-exception.filter';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Resolver(() => ViewGroupDTO)
@UseFilters(ViewGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ViewGroupResolver {
constructor(private readonly viewGroupService: ViewGroupService) {}
@Query(() => [ViewGroupDTO])
async getCoreViewGroups(
@AuthWorkspace() workspace: Workspace,
@Args('viewId', { type: () => String, nullable: true })
viewId?: string,
): Promise<ViewGroupDTO[]> {
if (viewId) {
return this.viewGroupService.findByViewId(workspace.id, viewId);
}
return this.viewGroupService.findByWorkspaceId(workspace.id);
}
@Query(() => ViewGroupDTO, { nullable: true })
async getCoreViewGroup(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewGroupDTO | null> {
return this.viewGroupService.findById(id, workspace.id);
}
@Mutation(() => ViewGroupDTO)
async createCoreViewGroup(
@Args('input') input: CreateViewGroupInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewGroupDTO> {
return this.viewGroupService.create({
...input,
workspaceId: workspace.id,
});
}
@Mutation(() => ViewGroupDTO)
async updateCoreViewGroup(
@Args('id', { type: () => String }) id: string,
@Args('input') input: UpdateViewGroupInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewGroupDTO> {
return this.viewGroupService.update(id, workspace.id, input);
}
@Mutation(() => Boolean)
async deleteCoreViewGroup(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<boolean> {
const deletedViewGroup = await this.viewGroupService.delete(
id,
workspace.id,
);
return isDefined(deletedViewGroup);
}
}
@@ -0,0 +1,71 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { isDefined } from 'twenty-shared/utils';
import { CreateViewSortInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-sort.input';
import { UpdateViewSortInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-sort.input';
import { ViewSortDTO } from 'src/engine/core-modules/view/dtos/view-sort.dto';
import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/core-modules/view/utils/view-graphql-api-exception.filter';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Resolver(() => ViewSortDTO)
@UseFilters(ViewGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ViewSortResolver {
constructor(private readonly viewSortService: ViewSortService) {}
@Query(() => [ViewSortDTO])
async getCoreViewSorts(
@AuthWorkspace() workspace: Workspace,
@Args('viewId', { type: () => String, nullable: true })
viewId?: string,
): Promise<ViewSortDTO[]> {
if (viewId) {
return this.viewSortService.findByViewId(workspace.id, viewId);
}
return this.viewSortService.findByWorkspaceId(workspace.id);
}
@Query(() => ViewSortDTO, { nullable: true })
async getCoreViewSort(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewSortDTO | null> {
return this.viewSortService.findById(id, workspace.id);
}
@Mutation(() => ViewSortDTO)
async createCoreViewSort(
@Args('input') input: CreateViewSortInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewSortDTO> {
return this.viewSortService.create({
...input,
workspaceId: workspace.id,
});
}
@Mutation(() => ViewSortDTO)
async updateCoreViewSort(
@Args('id', { type: () => String }) id: string,
@Args('input') input: UpdateViewSortInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewSortDTO> {
return this.viewSortService.update(id, workspace.id, input);
}
@Mutation(() => Boolean)
async deleteCoreViewSort(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<boolean> {
const deletedViewSort = await this.viewSortService.delete(id, workspace.id);
return isDefined(deletedViewSort);
}
}
@@ -0,0 +1,74 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { isDefined } from 'twenty-shared/utils';
import { CreateViewInput } from 'src/engine/core-modules/view/dtos/inputs/create-view.input';
import { UpdateViewInput } from 'src/engine/core-modules/view/dtos/inputs/update-view.input';
import { ViewDTO } from 'src/engine/core-modules/view/dtos/view.dto';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/core-modules/view/utils/view-graphql-api-exception.filter';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Resolver(() => ViewDTO)
@UseFilters(ViewGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ViewResolver {
constructor(private readonly viewService: ViewService) {}
@Query(() => [ViewDTO])
async getCoreViews(
@AuthWorkspace() workspace: Workspace,
@Args('objectMetadataId', { type: () => String, nullable: true })
objectMetadataId?: string,
): Promise<ViewDTO[]> {
if (objectMetadataId) {
return this.viewService.findByObjectMetadataId(
workspace.id,
objectMetadataId,
);
}
return this.viewService.findByWorkspaceId(workspace.id);
}
@Query(() => ViewDTO, { nullable: true })
async getCoreView(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewDTO | null> {
return this.viewService.findById(id, workspace.id);
}
@Mutation(() => ViewDTO)
async createCoreView(
@Args('input') input: CreateViewInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewDTO> {
return this.viewService.create({
...input,
workspaceId: workspace.id,
});
}
@Mutation(() => ViewDTO)
async updateCoreView(
@Args('id', { type: () => String }) id: string,
@Args('input') input: UpdateViewInput,
@AuthWorkspace() workspace: Workspace,
): Promise<ViewDTO> {
return this.viewService.update(id, workspace.id, input);
}
@Mutation(() => Boolean)
async deleteCoreView(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: Workspace,
): Promise<boolean> {
const deletedView = await this.viewService.delete(id, workspace.id);
return isDefined(deletedView);
}
}
@@ -0,0 +1,297 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ViewField } from 'src/engine/core-modules/view/entities/view-field.entity';
import {
ViewFieldException,
ViewFieldExceptionCode,
ViewFieldExceptionMessageKey,
generateViewFieldExceptionMessage,
generateViewFieldUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-field.exception';
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
describe('ViewFieldService', () => {
let viewFieldService: ViewFieldService;
let viewFieldRepository: Repository<ViewField>;
const mockViewField = {
id: 'view-field-id',
fieldMetadataId: 'field-id',
viewId: 'view-id',
workspaceId: 'workspace-id',
position: 0,
isVisible: true,
size: 100,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
} as ViewField;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ViewFieldService,
{
provide: getRepositoryToken(ViewField, 'core'),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
softDelete: jest.fn(),
},
},
],
}).compile();
viewFieldService = module.get<ViewFieldService>(ViewFieldService);
viewFieldRepository = module.get<Repository<ViewField>>(
getRepositoryToken(ViewField, 'core'),
);
});
it('should be defined', () => {
expect(viewFieldService).toBeDefined();
});
describe('findByWorkspaceId', () => {
it('should return view fields for a workspace', async () => {
const workspaceId = 'workspace-id';
const expectedViewFields = [mockViewField];
jest
.spyOn(viewFieldRepository, 'find')
.mockResolvedValue(expectedViewFields);
const result = await viewFieldService.findByWorkspaceId(workspaceId);
expect(viewFieldRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
deletedAt: expect.anything(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
expect(result).toEqual(expectedViewFields);
});
});
describe('findByViewId', () => {
it('should return view fields for a view', async () => {
const workspaceId = 'workspace-id';
const viewId = 'view-id';
const expectedViewFields = [mockViewField];
jest
.spyOn(viewFieldRepository, 'find')
.mockResolvedValue(expectedViewFields);
const result = await viewFieldService.findByViewId(workspaceId, viewId);
expect(viewFieldRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
viewId,
deletedAt: expect.anything(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
expect(result).toEqual(expectedViewFields);
});
});
describe('findById', () => {
it('should return a view field by id', async () => {
const id = 'view-field-id';
const workspaceId = 'workspace-id';
jest
.spyOn(viewFieldRepository, 'findOne')
.mockResolvedValue(mockViewField);
const result = await viewFieldService.findById(id, workspaceId);
expect(viewFieldRepository.findOne).toHaveBeenCalledWith({
where: {
id,
workspaceId,
deletedAt: expect.anything(),
},
relations: ['workspace', 'view'],
});
expect(result).toEqual(mockViewField);
});
it('should return null when view field is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewFieldRepository, 'findOne').mockResolvedValue(null);
const result = await viewFieldService.findById(id, workspaceId);
expect(result).toBeNull();
});
});
describe('create', () => {
const validViewFieldData = {
fieldMetadataId: 'field-id',
viewId: 'view-id',
workspaceId: 'workspace-id',
position: 0,
isVisible: true,
size: 100,
};
it('should create a view field successfully', async () => {
jest.spyOn(viewFieldRepository, 'create').mockReturnValue(mockViewField);
jest.spyOn(viewFieldRepository, 'save').mockResolvedValue(mockViewField);
const result = await viewFieldService.create(validViewFieldData);
expect(viewFieldRepository.create).toHaveBeenCalledWith(
validViewFieldData,
);
expect(viewFieldRepository.save).toHaveBeenCalledWith(mockViewField);
expect(result).toEqual(mockViewField);
});
it('should throw exception when workspaceId is missing', async () => {
const invalidData = { ...validViewFieldData, workspaceId: undefined };
await expect(viewFieldService.create(invalidData)).rejects.toThrow(
new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
),
);
});
it('should throw exception when viewId is missing', async () => {
const invalidData = { ...validViewFieldData, viewId: undefined };
await expect(viewFieldService.create(invalidData)).rejects.toThrow(
new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED,
),
},
),
);
});
it('should throw exception when fieldMetadataId is missing', async () => {
const invalidData = { ...validViewFieldData, fieldMetadataId: undefined };
await expect(viewFieldService.create(invalidData)).rejects.toThrow(
new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
},
),
);
});
});
describe('update', () => {
it('should update a view field successfully', async () => {
const id = 'view-field-id';
const workspaceId = 'workspace-id';
const updateData = { position: 1 };
const updatedViewField = { ...mockViewField, ...updateData };
jest.spyOn(viewFieldService, 'findById').mockResolvedValue(mockViewField);
jest
.spyOn(viewFieldRepository, 'save')
.mockResolvedValue(updatedViewField);
const result = await viewFieldService.update(id, workspaceId, updateData);
expect(viewFieldService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewFieldRepository.save).toHaveBeenCalledWith({
id,
...updateData,
});
expect(result).toEqual({ ...mockViewField, ...updatedViewField });
});
it('should throw exception when view field is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
const updateData = { position: 1 };
jest.spyOn(viewFieldService, 'findById').mockResolvedValue(null);
await expect(
viewFieldService.update(id, workspaceId, updateData),
).rejects.toThrow(
new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
),
);
});
});
describe('delete', () => {
it('should delete a view field successfully', async () => {
const id = 'view-field-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewFieldService, 'findById').mockResolvedValue(mockViewField);
jest
.spyOn(viewFieldRepository, 'softDelete')
.mockResolvedValue({} as any);
const result = await viewFieldService.delete(id, workspaceId);
expect(viewFieldService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewFieldRepository.softDelete).toHaveBeenCalledWith(id);
expect(result).toEqual(mockViewField);
});
it('should throw exception when view field is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewFieldService, 'findById').mockResolvedValue(null);
await expect(viewFieldService.delete(id, workspaceId)).rejects.toThrow(
new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
),
);
});
});
});
@@ -0,0 +1,332 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ViewFilterGroup } from 'src/engine/core-modules/view/entities/view-filter-group.entity';
import { ViewFilterGroupLogicalOperator } from 'src/engine/core-modules/view/enums/view-filter-group-logical-operator';
import {
ViewFilterGroupException,
ViewFilterGroupExceptionCode,
ViewFilterGroupExceptionMessageKey,
generateViewFilterGroupExceptionMessage,
generateViewFilterGroupUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-filter-group.exception';
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service';
describe('ViewFilterGroupService', () => {
let viewFilterGroupService: ViewFilterGroupService;
let viewFilterGroupRepository: Repository<ViewFilterGroup>;
const mockViewFilterGroup = {
id: 'view-filter-group-id',
viewId: 'view-id',
workspaceId: 'workspace-id',
logicalOperator: ViewFilterGroupLogicalOperator.AND,
positionInViewFilterGroup: 0,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
} as ViewFilterGroup;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ViewFilterGroupService,
{
provide: getRepositoryToken(ViewFilterGroup, 'core'),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
softDelete: jest.fn(),
},
},
],
}).compile();
viewFilterGroupService = module.get<ViewFilterGroupService>(
ViewFilterGroupService,
);
viewFilterGroupRepository = module.get<Repository<ViewFilterGroup>>(
getRepositoryToken(ViewFilterGroup, 'core'),
);
});
it('should be defined', () => {
expect(viewFilterGroupService).toBeDefined();
});
describe('findByWorkspaceId', () => {
it('should return view filter groups for a workspace', async () => {
const workspaceId = 'workspace-id';
const expectedViewFilterGroups = [mockViewFilterGroup];
jest
.spyOn(viewFilterGroupRepository, 'find')
.mockResolvedValue(expectedViewFilterGroups);
const result =
await viewFilterGroupService.findByWorkspaceId(workspaceId);
expect(viewFilterGroupRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
deletedAt: expect.anything(),
},
order: { positionInViewFilterGroup: 'ASC' },
relations: [
'workspace',
'view',
'viewFilters',
'parentViewFilterGroup',
'childViewFilterGroups',
],
});
expect(result).toEqual(expectedViewFilterGroups);
});
});
describe('findByViewId', () => {
it('should return view filter groups for a view', async () => {
const workspaceId = 'workspace-id';
const viewId = 'view-id';
const expectedViewFilterGroups = [mockViewFilterGroup];
jest
.spyOn(viewFilterGroupRepository, 'find')
.mockResolvedValue(expectedViewFilterGroups);
const result = await viewFilterGroupService.findByViewId(
workspaceId,
viewId,
);
expect(viewFilterGroupRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
viewId,
deletedAt: expect.anything(),
},
order: { positionInViewFilterGroup: 'ASC' },
relations: [
'workspace',
'view',
'viewFilters',
'parentViewFilterGroup',
'childViewFilterGroups',
],
});
expect(result).toEqual(expectedViewFilterGroups);
});
});
describe('findById', () => {
it('should return a view filter group by id', async () => {
const id = 'view-filter-group-id';
const workspaceId = 'workspace-id';
jest
.spyOn(viewFilterGroupRepository, 'findOne')
.mockResolvedValue(mockViewFilterGroup);
const result = await viewFilterGroupService.findById(id, workspaceId);
expect(viewFilterGroupRepository.findOne).toHaveBeenCalledWith({
where: {
id,
workspaceId,
deletedAt: expect.anything(),
},
relations: [
'workspace',
'view',
'viewFilters',
'parentViewFilterGroup',
'childViewFilterGroups',
],
});
expect(result).toEqual(mockViewFilterGroup);
});
it('should return null when view filter group is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewFilterGroupRepository, 'findOne').mockResolvedValue(null);
const result = await viewFilterGroupService.findById(id, workspaceId);
expect(result).toBeNull();
});
});
describe('create', () => {
const validViewFilterGroupData = {
viewId: 'view-id',
workspaceId: 'workspace-id',
logicalOperator: ViewFilterGroupLogicalOperator.AND,
positionInViewFilterGroup: 0,
};
it('should create a view filter group successfully', async () => {
jest
.spyOn(viewFilterGroupRepository, 'create')
.mockReturnValue(mockViewFilterGroup);
jest
.spyOn(viewFilterGroupRepository, 'save')
.mockResolvedValue(mockViewFilterGroup);
const result = await viewFilterGroupService.create(
validViewFilterGroupData,
);
expect(viewFilterGroupRepository.create).toHaveBeenCalledWith(
validViewFilterGroupData,
);
expect(viewFilterGroupRepository.save).toHaveBeenCalledWith(
mockViewFilterGroup,
);
expect(result).toEqual(mockViewFilterGroup);
});
it('should throw exception when workspaceId is missing', async () => {
const invalidData = {
...validViewFilterGroupData,
workspaceId: undefined,
};
await expect(viewFilterGroupService.create(invalidData)).rejects.toThrow(
new ViewFilterGroupException(
generateViewFilterGroupExceptionMessage(
ViewFilterGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewFilterGroupExceptionCode.INVALID_VIEW_FILTER_GROUP_DATA,
{
userFriendlyMessage:
generateViewFilterGroupUserFriendlyExceptionMessage(
ViewFilterGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
),
);
});
it('should throw exception when viewId is missing', async () => {
const invalidData = { ...validViewFilterGroupData, viewId: undefined };
await expect(viewFilterGroupService.create(invalidData)).rejects.toThrow(
new ViewFilterGroupException(
generateViewFilterGroupExceptionMessage(
ViewFilterGroupExceptionMessageKey.VIEW_ID_REQUIRED,
),
ViewFilterGroupExceptionCode.INVALID_VIEW_FILTER_GROUP_DATA,
{
userFriendlyMessage:
generateViewFilterGroupUserFriendlyExceptionMessage(
ViewFilterGroupExceptionMessageKey.VIEW_ID_REQUIRED,
),
},
),
);
});
});
describe('update', () => {
it('should update a view filter group successfully', async () => {
const id = 'view-filter-group-id';
const workspaceId = 'workspace-id';
const updateData = { logicalOperator: ViewFilterGroupLogicalOperator.OR };
const updatedViewFilterGroup = { ...mockViewFilterGroup, ...updateData };
jest
.spyOn(viewFilterGroupService, 'findById')
.mockResolvedValue(mockViewFilterGroup);
jest
.spyOn(viewFilterGroupRepository, 'save')
.mockResolvedValue(updatedViewFilterGroup);
const result = await viewFilterGroupService.update(
id,
workspaceId,
updateData,
);
expect(viewFilterGroupService.findById).toHaveBeenCalledWith(
id,
workspaceId,
);
expect(viewFilterGroupRepository.save).toHaveBeenCalledWith({
id,
...updateData,
});
expect(result).toEqual({
...mockViewFilterGroup,
...updatedViewFilterGroup,
});
});
it('should throw exception when view filter group is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
const updateData = { logicalOperator: ViewFilterGroupLogicalOperator.OR };
jest.spyOn(viewFilterGroupService, 'findById').mockResolvedValue(null);
await expect(
viewFilterGroupService.update(id, workspaceId, updateData),
).rejects.toThrow(
new ViewFilterGroupException(
generateViewFilterGroupExceptionMessage(
ViewFilterGroupExceptionMessageKey.VIEW_FILTER_GROUP_NOT_FOUND,
id,
),
ViewFilterGroupExceptionCode.VIEW_FILTER_GROUP_NOT_FOUND,
),
);
});
});
describe('delete', () => {
it('should delete a view filter group successfully', async () => {
const id = 'view-filter-group-id';
const workspaceId = 'workspace-id';
jest
.spyOn(viewFilterGroupService, 'findById')
.mockResolvedValue(mockViewFilterGroup);
jest
.spyOn(viewFilterGroupRepository, 'softDelete')
.mockResolvedValue({} as any);
const result = await viewFilterGroupService.delete(id, workspaceId);
expect(viewFilterGroupService.findById).toHaveBeenCalledWith(
id,
workspaceId,
);
expect(viewFilterGroupRepository.softDelete).toHaveBeenCalledWith(id);
expect(result).toEqual(mockViewFilterGroup);
});
it('should throw exception when view filter group is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewFilterGroupService, 'findById').mockResolvedValue(null);
await expect(
viewFilterGroupService.delete(id, workspaceId),
).rejects.toThrow(
new ViewFilterGroupException(
generateViewFilterGroupExceptionMessage(
ViewFilterGroupExceptionMessageKey.VIEW_FILTER_GROUP_NOT_FOUND,
id,
),
ViewFilterGroupExceptionCode.VIEW_FILTER_GROUP_NOT_FOUND,
),
);
});
});
});
@@ -0,0 +1,313 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ViewFilter } from 'src/engine/core-modules/view/entities/view-filter.entity';
import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand';
import {
ViewFilterException,
ViewFilterExceptionCode,
ViewFilterExceptionMessageKey,
generateViewFilterExceptionMessage,
generateViewFilterUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-filter.exception';
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
describe('ViewFilterService', () => {
let viewFilterService: ViewFilterService;
let viewFilterRepository: Repository<ViewFilter>;
const mockViewFilter = {
id: 'view-filter-id',
fieldMetadataId: 'field-id',
viewId: 'view-id',
workspaceId: 'workspace-id',
operand: ViewFilterOperand.CONTAINS,
value: 'test',
positionInViewFilterGroup: 0,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
} as ViewFilter;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ViewFilterService,
{
provide: getRepositoryToken(ViewFilter, 'core'),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
softDelete: jest.fn(),
},
},
],
}).compile();
viewFilterService = module.get<ViewFilterService>(ViewFilterService);
viewFilterRepository = module.get<Repository<ViewFilter>>(
getRepositoryToken(ViewFilter, 'core'),
);
});
it('should be defined', () => {
expect(viewFilterService).toBeDefined();
});
describe('findByWorkspaceId', () => {
it('should return view filters for a workspace', async () => {
const workspaceId = 'workspace-id';
const expectedViewFilters = [mockViewFilter];
jest
.spyOn(viewFilterRepository, 'find')
.mockResolvedValue(expectedViewFilters);
const result = await viewFilterService.findByWorkspaceId(workspaceId);
expect(viewFilterRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
deletedAt: expect.anything(),
},
order: { positionInViewFilterGroup: 'ASC' },
relations: ['workspace', 'view', 'viewFilterGroup'],
});
expect(result).toEqual(expectedViewFilters);
});
});
describe('findByViewId', () => {
it('should return view filters for a view', async () => {
const workspaceId = 'workspace-id';
const viewId = 'view-id';
const expectedViewFilters = [mockViewFilter];
jest
.spyOn(viewFilterRepository, 'find')
.mockResolvedValue(expectedViewFilters);
const result = await viewFilterService.findByViewId(workspaceId, viewId);
expect(viewFilterRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
viewId,
deletedAt: expect.anything(),
},
order: { positionInViewFilterGroup: 'ASC' },
relations: ['workspace', 'view', 'viewFilterGroup'],
});
expect(result).toEqual(expectedViewFilters);
});
});
describe('findById', () => {
it('should return a view filter by id', async () => {
const id = 'view-filter-id';
const workspaceId = 'workspace-id';
jest
.spyOn(viewFilterRepository, 'findOne')
.mockResolvedValue(mockViewFilter);
const result = await viewFilterService.findById(id, workspaceId);
expect(viewFilterRepository.findOne).toHaveBeenCalledWith({
where: {
id,
workspaceId,
deletedAt: expect.anything(),
},
relations: ['workspace', 'view', 'viewFilterGroup'],
});
expect(result).toEqual(mockViewFilter);
});
it('should return null when view filter is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewFilterRepository, 'findOne').mockResolvedValue(null);
const result = await viewFilterService.findById(id, workspaceId);
expect(result).toBeNull();
});
});
describe('create', () => {
const validViewFilterData = {
fieldMetadataId: 'field-id',
viewId: 'view-id',
workspaceId: 'workspace-id',
operand: ViewFilterOperand.CONTAINS,
value: 'test',
positionInViewFilterGroup: 0,
};
it('should create a view filter successfully', async () => {
jest
.spyOn(viewFilterRepository, 'create')
.mockReturnValue(mockViewFilter);
jest
.spyOn(viewFilterRepository, 'save')
.mockResolvedValue(mockViewFilter);
const result = await viewFilterService.create(validViewFilterData);
expect(viewFilterRepository.create).toHaveBeenCalledWith(
validViewFilterData,
);
expect(viewFilterRepository.save).toHaveBeenCalledWith(mockViewFilter);
expect(result).toEqual(mockViewFilter);
});
it('should throw exception when workspaceId is missing', async () => {
const invalidData = { ...validViewFilterData, workspaceId: undefined };
await expect(viewFilterService.create(invalidData)).rejects.toThrow(
new ViewFilterException(
generateViewFilterExceptionMessage(
ViewFilterExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
{
userFriendlyMessage: generateViewFilterUserFriendlyExceptionMessage(
ViewFilterExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
),
);
});
it('should throw exception when viewId is missing', async () => {
const invalidData = { ...validViewFilterData, viewId: undefined };
await expect(viewFilterService.create(invalidData)).rejects.toThrow(
new ViewFilterException(
generateViewFilterExceptionMessage(
ViewFilterExceptionMessageKey.VIEW_ID_REQUIRED,
),
ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
{
userFriendlyMessage: generateViewFilterUserFriendlyExceptionMessage(
ViewFilterExceptionMessageKey.VIEW_ID_REQUIRED,
),
},
),
);
});
it('should throw exception when fieldMetadataId is missing', async () => {
const invalidData = {
...validViewFilterData,
fieldMetadataId: undefined,
};
await expect(viewFilterService.create(invalidData)).rejects.toThrow(
new ViewFilterException(
generateViewFilterExceptionMessage(
ViewFilterExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
{
userFriendlyMessage: generateViewFilterUserFriendlyExceptionMessage(
ViewFilterExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
},
),
);
});
});
describe('update', () => {
it('should update a view filter successfully', async () => {
const id = 'view-filter-id';
const workspaceId = 'workspace-id';
const updateData = { value: 'updated test' };
const updatedViewFilter = { ...mockViewFilter, ...updateData };
jest
.spyOn(viewFilterService, 'findById')
.mockResolvedValue(mockViewFilter);
jest
.spyOn(viewFilterRepository, 'save')
.mockResolvedValue(updatedViewFilter);
const result = await viewFilterService.update(
id,
workspaceId,
updateData,
);
expect(viewFilterService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewFilterRepository.save).toHaveBeenCalledWith({
id,
...updateData,
});
expect(result).toEqual({ ...mockViewFilter, ...updatedViewFilter });
});
it('should throw exception when view filter is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
const updateData = { value: 'updated test' };
jest.spyOn(viewFilterService, 'findById').mockResolvedValue(null);
await expect(
viewFilterService.update(id, workspaceId, updateData),
).rejects.toThrow(
new ViewFilterException(
generateViewFilterExceptionMessage(
ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND,
id,
),
ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND,
),
);
});
});
describe('delete', () => {
it('should delete a view filter successfully', async () => {
const id = 'view-filter-id';
const workspaceId = 'workspace-id';
jest
.spyOn(viewFilterService, 'findById')
.mockResolvedValue(mockViewFilter);
jest
.spyOn(viewFilterRepository, 'softDelete')
.mockResolvedValue({} as any);
const result = await viewFilterService.delete(id, workspaceId);
expect(viewFilterService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewFilterRepository.softDelete).toHaveBeenCalledWith(id);
expect(result).toEqual(mockViewFilter);
});
it('should throw exception when view filter is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewFilterService, 'findById').mockResolvedValue(null);
await expect(viewFilterService.delete(id, workspaceId)).rejects.toThrow(
new ViewFilterException(
generateViewFilterExceptionMessage(
ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND,
id,
),
ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND,
),
);
});
});
});
@@ -0,0 +1,297 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ViewGroup } from 'src/engine/core-modules/view/entities/view-group.entity';
import {
ViewGroupException,
ViewGroupExceptionCode,
ViewGroupExceptionMessageKey,
generateViewGroupExceptionMessage,
generateViewGroupUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-group.exception';
import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
describe('ViewGroupService', () => {
let viewGroupService: ViewGroupService;
let viewGroupRepository: Repository<ViewGroup>;
const mockViewGroup = {
id: 'view-group-id',
fieldMetadataId: 'field-id',
viewId: 'view-id',
workspaceId: 'workspace-id',
fieldValue: 'group-value',
isVisible: true,
position: 0,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
} as ViewGroup;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ViewGroupService,
{
provide: getRepositoryToken(ViewGroup, 'core'),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
softDelete: jest.fn(),
},
},
],
}).compile();
viewGroupService = module.get<ViewGroupService>(ViewGroupService);
viewGroupRepository = module.get<Repository<ViewGroup>>(
getRepositoryToken(ViewGroup, 'core'),
);
});
it('should be defined', () => {
expect(viewGroupService).toBeDefined();
});
describe('findByWorkspaceId', () => {
it('should return view groups for a workspace', async () => {
const workspaceId = 'workspace-id';
const expectedViewGroups = [mockViewGroup];
jest
.spyOn(viewGroupRepository, 'find')
.mockResolvedValue(expectedViewGroups);
const result = await viewGroupService.findByWorkspaceId(workspaceId);
expect(viewGroupRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
deletedAt: expect.anything(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
expect(result).toEqual(expectedViewGroups);
});
});
describe('findByViewId', () => {
it('should return view groups for a view', async () => {
const workspaceId = 'workspace-id';
const viewId = 'view-id';
const expectedViewGroups = [mockViewGroup];
jest
.spyOn(viewGroupRepository, 'find')
.mockResolvedValue(expectedViewGroups);
const result = await viewGroupService.findByViewId(workspaceId, viewId);
expect(viewGroupRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
viewId,
deletedAt: expect.anything(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
expect(result).toEqual(expectedViewGroups);
});
});
describe('findById', () => {
it('should return a view group by id', async () => {
const id = 'view-group-id';
const workspaceId = 'workspace-id';
jest
.spyOn(viewGroupRepository, 'findOne')
.mockResolvedValue(mockViewGroup);
const result = await viewGroupService.findById(id, workspaceId);
expect(viewGroupRepository.findOne).toHaveBeenCalledWith({
where: {
id,
workspaceId,
deletedAt: expect.anything(),
},
relations: ['workspace', 'view'],
});
expect(result).toEqual(mockViewGroup);
});
it('should return null when view group is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewGroupRepository, 'findOne').mockResolvedValue(null);
const result = await viewGroupService.findById(id, workspaceId);
expect(result).toBeNull();
});
});
describe('create', () => {
const validViewGroupData = {
fieldMetadataId: 'field-id',
viewId: 'view-id',
workspaceId: 'workspace-id',
fieldValue: 'group-value',
isVisible: true,
position: 0,
};
it('should create a view group successfully', async () => {
jest.spyOn(viewGroupRepository, 'create').mockReturnValue(mockViewGroup);
jest.spyOn(viewGroupRepository, 'save').mockResolvedValue(mockViewGroup);
const result = await viewGroupService.create(validViewGroupData);
expect(viewGroupRepository.create).toHaveBeenCalledWith(
validViewGroupData,
);
expect(viewGroupRepository.save).toHaveBeenCalledWith(mockViewGroup);
expect(result).toEqual(mockViewGroup);
});
it('should throw exception when workspaceId is missing', async () => {
const invalidData = { ...validViewGroupData, workspaceId: undefined };
await expect(viewGroupService.create(invalidData)).rejects.toThrow(
new ViewGroupException(
generateViewGroupExceptionMessage(
ViewGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA,
{
userFriendlyMessage: generateViewGroupUserFriendlyExceptionMessage(
ViewGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
),
);
});
it('should throw exception when viewId is missing', async () => {
const invalidData = { ...validViewGroupData, viewId: undefined };
await expect(viewGroupService.create(invalidData)).rejects.toThrow(
new ViewGroupException(
generateViewGroupExceptionMessage(
ViewGroupExceptionMessageKey.VIEW_ID_REQUIRED,
),
ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA,
{
userFriendlyMessage: generateViewGroupUserFriendlyExceptionMessage(
ViewGroupExceptionMessageKey.VIEW_ID_REQUIRED,
),
},
),
);
});
it('should throw exception when fieldMetadataId is missing', async () => {
const invalidData = { ...validViewGroupData, fieldMetadataId: undefined };
await expect(viewGroupService.create(invalidData)).rejects.toThrow(
new ViewGroupException(
generateViewGroupExceptionMessage(
ViewGroupExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA,
{
userFriendlyMessage: generateViewGroupUserFriendlyExceptionMessage(
ViewGroupExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
},
),
);
});
});
describe('update', () => {
it('should update a view group successfully', async () => {
const id = 'view-group-id';
const workspaceId = 'workspace-id';
const updateData = { isVisible: false };
const updatedViewGroup = { ...mockViewGroup, ...updateData };
jest.spyOn(viewGroupService, 'findById').mockResolvedValue(mockViewGroup);
jest
.spyOn(viewGroupRepository, 'save')
.mockResolvedValue(updatedViewGroup);
const result = await viewGroupService.update(id, workspaceId, updateData);
expect(viewGroupService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewGroupRepository.save).toHaveBeenCalledWith({
id,
...updateData,
});
expect(result).toEqual({ ...mockViewGroup, ...updatedViewGroup });
});
it('should throw exception when view group is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
const updateData = { isVisible: false };
jest.spyOn(viewGroupService, 'findById').mockResolvedValue(null);
await expect(
viewGroupService.update(id, workspaceId, updateData),
).rejects.toThrow(
new ViewGroupException(
generateViewGroupExceptionMessage(
ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND,
id,
),
ViewGroupExceptionCode.VIEW_GROUP_NOT_FOUND,
),
);
});
});
describe('delete', () => {
it('should delete a view group successfully', async () => {
const id = 'view-group-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewGroupService, 'findById').mockResolvedValue(mockViewGroup);
jest
.spyOn(viewGroupRepository, 'softDelete')
.mockResolvedValue({} as any);
const result = await viewGroupService.delete(id, workspaceId);
expect(viewGroupService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewGroupRepository.softDelete).toHaveBeenCalledWith(id);
expect(result).toEqual(mockViewGroup);
});
it('should throw exception when view group is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewGroupService, 'findById').mockResolvedValue(null);
await expect(viewGroupService.delete(id, workspaceId)).rejects.toThrow(
new ViewGroupException(
generateViewGroupExceptionMessage(
ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND,
id,
),
ViewGroupExceptionCode.VIEW_GROUP_NOT_FOUND,
),
);
});
});
});
@@ -0,0 +1,284 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ViewSort } from 'src/engine/core-modules/view/entities/view-sort.entity';
import { ViewSortDirection } from 'src/engine/core-modules/view/enums/view-sort-direction';
import {
ViewSortException,
ViewSortExceptionCode,
ViewSortExceptionMessageKey,
generateViewSortExceptionMessage,
generateViewSortUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-sort.exception';
import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service';
describe('ViewSortService', () => {
let viewSortService: ViewSortService;
let viewSortRepository: Repository<ViewSort>;
const mockViewSort = {
id: 'view-sort-id',
fieldMetadataId: 'field-id',
viewId: 'view-id',
workspaceId: 'workspace-id',
direction: ViewSortDirection.ASC,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
} as ViewSort;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ViewSortService,
{
provide: getRepositoryToken(ViewSort, 'core'),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
softDelete: jest.fn(),
},
},
],
}).compile();
viewSortService = module.get<ViewSortService>(ViewSortService);
viewSortRepository = module.get<Repository<ViewSort>>(
getRepositoryToken(ViewSort, 'core'),
);
});
it('should be defined', () => {
expect(viewSortService).toBeDefined();
});
describe('findByWorkspaceId', () => {
it('should return view sorts for a workspace', async () => {
const workspaceId = 'workspace-id';
const expectedViewSorts = [mockViewSort];
jest
.spyOn(viewSortRepository, 'find')
.mockResolvedValue(expectedViewSorts);
const result = await viewSortService.findByWorkspaceId(workspaceId);
expect(viewSortRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
deletedAt: expect.anything(),
},
relations: ['workspace', 'view'],
});
expect(result).toEqual(expectedViewSorts);
});
});
describe('findByViewId', () => {
it('should return view sorts for a view', async () => {
const workspaceId = 'workspace-id';
const viewId = 'view-id';
const expectedViewSorts = [mockViewSort];
jest
.spyOn(viewSortRepository, 'find')
.mockResolvedValue(expectedViewSorts);
const result = await viewSortService.findByViewId(workspaceId, viewId);
expect(viewSortRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
viewId,
deletedAt: expect.anything(),
},
relations: ['workspace', 'view'],
});
expect(result).toEqual(expectedViewSorts);
});
});
describe('findById', () => {
it('should return a view sort by id', async () => {
const id = 'view-sort-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewSortRepository, 'findOne').mockResolvedValue(mockViewSort);
const result = await viewSortService.findById(id, workspaceId);
expect(viewSortRepository.findOne).toHaveBeenCalledWith({
where: {
id,
workspaceId,
deletedAt: expect.anything(),
},
relations: ['workspace', 'view'],
});
expect(result).toEqual(mockViewSort);
});
it('should return null when view sort is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewSortRepository, 'findOne').mockResolvedValue(null);
const result = await viewSortService.findById(id, workspaceId);
expect(result).toBeNull();
});
});
describe('create', () => {
const validViewSortData = {
fieldMetadataId: 'field-id',
viewId: 'view-id',
workspaceId: 'workspace-id',
direction: ViewSortDirection.ASC,
};
it('should create a view sort successfully', async () => {
jest.spyOn(viewSortRepository, 'create').mockReturnValue(mockViewSort);
jest.spyOn(viewSortRepository, 'save').mockResolvedValue(mockViewSort);
const result = await viewSortService.create(validViewSortData);
expect(viewSortRepository.create).toHaveBeenCalledWith(validViewSortData);
expect(viewSortRepository.save).toHaveBeenCalledWith(mockViewSort);
expect(result).toEqual(mockViewSort);
});
it('should throw exception when workspaceId is missing', async () => {
const invalidData = { ...validViewSortData, workspaceId: undefined };
await expect(viewSortService.create(invalidData)).rejects.toThrow(
new ViewSortException(
generateViewSortExceptionMessage(
ViewSortExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewSortExceptionCode.INVALID_VIEW_SORT_DATA,
{
userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage(
ViewSortExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
),
);
});
it('should throw exception when viewId is missing', async () => {
const invalidData = { ...validViewSortData, viewId: undefined };
await expect(viewSortService.create(invalidData)).rejects.toThrow(
new ViewSortException(
generateViewSortExceptionMessage(
ViewSortExceptionMessageKey.VIEW_ID_REQUIRED,
),
ViewSortExceptionCode.INVALID_VIEW_SORT_DATA,
{
userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage(
ViewSortExceptionMessageKey.VIEW_ID_REQUIRED,
),
},
),
);
});
it('should throw exception when fieldMetadataId is missing', async () => {
const invalidData = { ...validViewSortData, fieldMetadataId: undefined };
await expect(viewSortService.create(invalidData)).rejects.toThrow(
new ViewSortException(
generateViewSortExceptionMessage(
ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
ViewSortExceptionCode.INVALID_VIEW_SORT_DATA,
{
userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage(
ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
},
),
);
});
});
describe('update', () => {
it('should update a view sort successfully', async () => {
const id = 'view-sort-id';
const workspaceId = 'workspace-id';
const updateData = { direction: ViewSortDirection.DESC };
const updatedViewSort = { ...mockViewSort, ...updateData };
jest.spyOn(viewSortService, 'findById').mockResolvedValue(mockViewSort);
jest.spyOn(viewSortRepository, 'save').mockResolvedValue(updatedViewSort);
const result = await viewSortService.update(id, workspaceId, updateData);
expect(viewSortService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewSortRepository.save).toHaveBeenCalledWith({
id,
...updateData,
});
expect(result).toEqual({ ...mockViewSort, ...updatedViewSort });
});
it('should throw exception when view sort is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
const updateData = { direction: ViewSortDirection.DESC };
jest.spyOn(viewSortService, 'findById').mockResolvedValue(null);
await expect(
viewSortService.update(id, workspaceId, updateData),
).rejects.toThrow(
new ViewSortException(
generateViewSortExceptionMessage(
ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND,
id,
),
ViewSortExceptionCode.VIEW_SORT_NOT_FOUND,
),
);
});
});
describe('delete', () => {
it('should delete a view sort successfully', async () => {
const id = 'view-sort-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewSortService, 'findById').mockResolvedValue(mockViewSort);
jest.spyOn(viewSortRepository, 'softDelete').mockResolvedValue({} as any);
const result = await viewSortService.delete(id, workspaceId);
expect(viewSortService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewSortRepository.softDelete).toHaveBeenCalledWith(id);
expect(result).toEqual(mockViewSort);
});
it('should throw exception when view sort is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewSortService, 'findById').mockResolvedValue(null);
await expect(viewSortService.delete(id, workspaceId)).rejects.toThrow(
new ViewSortException(
generateViewSortExceptionMessage(
ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND,
id,
),
ViewSortExceptionCode.VIEW_SORT_NOT_FOUND,
),
);
});
});
});
@@ -0,0 +1,298 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { View } from 'src/engine/core-modules/view/entities/view.entity';
import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in';
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
import {
ViewException,
ViewExceptionCode,
ViewExceptionMessageKey,
generateViewExceptionMessage,
generateViewUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view.exception';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
describe('ViewService', () => {
let viewService: ViewService;
let viewRepository: Repository<View>;
const mockView = {
id: 'view-id',
name: 'Test View',
objectMetadataId: 'object-id',
workspaceId: 'workspace-id',
type: ViewType.TABLE,
icon: 'test-icon',
position: 0,
isCompact: false,
key: 'INDEX',
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
kanbanAggregateOperation: null,
kanbanAggregateOperationFieldMetadataId: null,
anyFieldFilterValue: null,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
} as View;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ViewService,
{
provide: getRepositoryToken(View, 'core'),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
softDelete: jest.fn(),
},
},
],
}).compile();
viewService = module.get<ViewService>(ViewService);
viewRepository = module.get<Repository<View>>(
getRepositoryToken(View, 'core'),
);
});
it('should be defined', () => {
expect(viewService).toBeDefined();
});
describe('findByWorkspaceId', () => {
it('should return views for a workspace', async () => {
const workspaceId = 'workspace-id';
const expectedViews = [mockView];
jest.spyOn(viewRepository, 'find').mockResolvedValue(expectedViews);
const result = await viewService.findByWorkspaceId(workspaceId);
expect(viewRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
deletedAt: expect.anything(),
},
order: { position: 'ASC' },
relations: [
'workspace',
'viewFields',
'viewFilters',
'viewSorts',
'viewGroups',
'viewFilterGroups',
],
});
expect(result).toEqual(expectedViews);
});
});
describe('findByObjectMetadataId', () => {
it('should return views for an object metadata id', async () => {
const workspaceId = 'workspace-id';
const objectMetadataId = 'object-id';
const expectedViews = [mockView];
jest.spyOn(viewRepository, 'find').mockResolvedValue(expectedViews);
const result = await viewService.findByObjectMetadataId(
workspaceId,
objectMetadataId,
);
expect(viewRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
objectMetadataId,
deletedAt: expect.anything(),
},
order: { position: 'ASC' },
relations: [
'workspace',
'viewFields',
'viewFilters',
'viewSorts',
'viewGroups',
'viewFilterGroups',
],
});
expect(result).toEqual(expectedViews);
});
});
describe('findById', () => {
it('should return a view by id', async () => {
const id = 'view-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewRepository, 'findOne').mockResolvedValue(mockView);
const result = await viewService.findById(id, workspaceId);
expect(viewRepository.findOne).toHaveBeenCalledWith({
where: {
id,
workspaceId,
deletedAt: expect.anything(),
},
relations: [
'workspace',
'viewFields',
'viewFilters',
'viewSorts',
'viewGroups',
'viewFilterGroups',
],
});
expect(result).toEqual(mockView);
});
it('should return null when view is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewRepository, 'findOne').mockResolvedValue(null);
const result = await viewService.findById(id, workspaceId);
expect(result).toBeNull();
});
});
describe('create', () => {
const validViewData = {
name: 'Test View',
workspaceId: 'workspace-id',
objectMetadataId: 'object-id',
type: ViewType.TABLE,
icon: 'test-icon',
};
it('should create a view successfully', async () => {
jest.spyOn(viewRepository, 'create').mockReturnValue(mockView);
jest.spyOn(viewRepository, 'save').mockResolvedValue(mockView);
const result = await viewService.create(validViewData);
expect(viewRepository.create).toHaveBeenCalledWith(validViewData);
expect(viewRepository.save).toHaveBeenCalledWith(mockView);
expect(result).toEqual(mockView);
});
it('should throw exception when workspaceId is missing', async () => {
const invalidData = { ...validViewData, workspaceId: undefined };
await expect(viewService.create(invalidData)).rejects.toThrow(
new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewExceptionCode.INVALID_VIEW_DATA,
{
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
ViewExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
),
);
});
it('should throw exception when objectMetadataId is missing', async () => {
const invalidData = { ...validViewData, objectMetadataId: undefined };
await expect(viewService.create(invalidData)).rejects.toThrow(
new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.OBJECT_METADATA_ID_REQUIRED,
),
ViewExceptionCode.INVALID_VIEW_DATA,
{
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
ViewExceptionMessageKey.OBJECT_METADATA_ID_REQUIRED,
),
},
),
);
});
});
describe('update', () => {
it('should update a view successfully', async () => {
const id = 'view-id';
const workspaceId = 'workspace-id';
const updateData = { name: 'Updated View' };
const updatedView = { ...mockView, ...updateData };
jest.spyOn(viewService, 'findById').mockResolvedValue(mockView);
jest.spyOn(viewRepository, 'save').mockResolvedValue(updatedView);
const result = await viewService.update(id, workspaceId, updateData);
expect(viewService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewRepository.save).toHaveBeenCalledWith({
id,
...updateData,
});
expect(result).toEqual({ ...mockView, ...updatedView });
});
it('should throw exception when view is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
const updateData = { name: 'Updated View' };
jest.spyOn(viewService, 'findById').mockResolvedValue(null);
await expect(
viewService.update(id, workspaceId, updateData),
).rejects.toThrow(
new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.VIEW_NOT_FOUND,
id,
),
ViewExceptionCode.VIEW_NOT_FOUND,
),
);
});
});
describe('delete', () => {
it('should delete a view successfully', async () => {
const id = 'view-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewService, 'findById').mockResolvedValue(mockView);
jest.spyOn(viewRepository, 'softDelete').mockResolvedValue({} as any);
const result = await viewService.delete(id, workspaceId);
expect(viewService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewRepository.softDelete).toHaveBeenCalledWith(id);
expect(result).toEqual(mockView);
});
it('should throw exception when view is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewService, 'findById').mockResolvedValue(null);
await expect(viewService.delete(id, workspaceId)).rejects.toThrow(
new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.VIEW_NOT_FOUND,
id,
),
ViewExceptionCode.VIEW_NOT_FOUND,
),
);
});
});
});
@@ -0,0 +1,152 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { ViewField } from 'src/engine/core-modules/view/entities/view-field.entity';
import {
ViewFieldException,
ViewFieldExceptionCode,
ViewFieldExceptionMessageKey,
generateViewFieldExceptionMessage,
generateViewFieldUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-field.exception';
@Injectable()
export class ViewFieldService {
constructor(
@InjectRepository(ViewField, 'core')
private readonly viewFieldRepository: Repository<ViewField>,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewField[]> {
return this.viewFieldRepository.find({
where: {
workspaceId,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
}
async findByViewId(
workspaceId: string,
viewId: string,
): Promise<ViewField[]> {
return this.viewFieldRepository.find({
where: {
workspaceId,
viewId,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
}
async findById(id: string, workspaceId: string): Promise<ViewField | null> {
const viewField = await this.viewFieldRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
relations: ['workspace', 'view'],
});
return viewField || null;
}
async create(viewFieldData: Partial<ViewField>): Promise<ViewField> {
if (!isDefined(viewFieldData.workspaceId)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
);
}
if (!isDefined(viewFieldData.viewId)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED,
),
},
);
}
if (!isDefined(viewFieldData.fieldMetadataId)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
},
);
}
const viewField = this.viewFieldRepository.create(viewFieldData);
return this.viewFieldRepository.save(viewField);
}
async update(
id: string,
workspaceId: string,
updateData: Partial<ViewField>,
): Promise<ViewField> {
const existingViewField = await this.findById(id, workspaceId);
if (!isDefined(existingViewField)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
const updatedViewField = await this.viewFieldRepository.save({
id,
...updateData,
});
return { ...existingViewField, ...updatedViewField };
}
async delete(id: string, workspaceId: string): Promise<ViewField> {
const viewField = await this.findById(id, workspaceId);
if (!isDefined(viewField)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
await this.viewFieldRepository.softDelete(id);
return viewField;
}
}
@@ -0,0 +1,164 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { ViewFilterGroup } from 'src/engine/core-modules/view/entities/view-filter-group.entity';
import {
ViewFilterGroupException,
ViewFilterGroupExceptionCode,
ViewFilterGroupExceptionMessageKey,
generateViewFilterGroupExceptionMessage,
generateViewFilterGroupUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-filter-group.exception';
@Injectable()
export class ViewFilterGroupService {
constructor(
@InjectRepository(ViewFilterGroup, 'core')
private readonly viewFilterGroupRepository: Repository<ViewFilterGroup>,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewFilterGroup[]> {
return this.viewFilterGroupRepository.find({
where: {
workspaceId,
deletedAt: IsNull(),
},
order: { positionInViewFilterGroup: 'ASC' },
relations: [
'workspace',
'view',
'viewFilters',
'parentViewFilterGroup',
'childViewFilterGroups',
],
});
}
async findByViewId(
workspaceId: string,
viewId: string,
): Promise<ViewFilterGroup[]> {
return this.viewFilterGroupRepository.find({
where: {
workspaceId,
viewId,
deletedAt: IsNull(),
},
order: { positionInViewFilterGroup: 'ASC' },
relations: [
'workspace',
'view',
'viewFilters',
'parentViewFilterGroup',
'childViewFilterGroups',
],
});
}
async findById(
id: string,
workspaceId: string,
): Promise<ViewFilterGroup | null> {
const viewFilterGroup = await this.viewFilterGroupRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
relations: [
'workspace',
'view',
'viewFilters',
'parentViewFilterGroup',
'childViewFilterGroups',
],
});
return viewFilterGroup || null;
}
async create(
viewFilterGroupData: Partial<ViewFilterGroup>,
): Promise<ViewFilterGroup> {
if (!isDefined(viewFilterGroupData.workspaceId)) {
throw new ViewFilterGroupException(
generateViewFilterGroupExceptionMessage(
ViewFilterGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewFilterGroupExceptionCode.INVALID_VIEW_FILTER_GROUP_DATA,
{
userFriendlyMessage:
generateViewFilterGroupUserFriendlyExceptionMessage(
ViewFilterGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
);
}
if (!isDefined(viewFilterGroupData.viewId)) {
throw new ViewFilterGroupException(
generateViewFilterGroupExceptionMessage(
ViewFilterGroupExceptionMessageKey.VIEW_ID_REQUIRED,
),
ViewFilterGroupExceptionCode.INVALID_VIEW_FILTER_GROUP_DATA,
{
userFriendlyMessage:
generateViewFilterGroupUserFriendlyExceptionMessage(
ViewFilterGroupExceptionMessageKey.VIEW_ID_REQUIRED,
),
},
);
}
const viewFilterGroup =
this.viewFilterGroupRepository.create(viewFilterGroupData);
return this.viewFilterGroupRepository.save(viewFilterGroup);
}
async update(
id: string,
workspaceId: string,
updateData: Partial<ViewFilterGroup>,
): Promise<ViewFilterGroup> {
const existingViewFilterGroup = await this.findById(id, workspaceId);
if (!isDefined(existingViewFilterGroup)) {
throw new ViewFilterGroupException(
generateViewFilterGroupExceptionMessage(
ViewFilterGroupExceptionMessageKey.VIEW_FILTER_GROUP_NOT_FOUND,
id,
),
ViewFilterGroupExceptionCode.VIEW_FILTER_GROUP_NOT_FOUND,
);
}
const updatedViewFilterGroup = await this.viewFilterGroupRepository.save({
id,
...updateData,
});
return { ...existingViewFilterGroup, ...updatedViewFilterGroup };
}
async delete(id: string, workspaceId: string): Promise<ViewFilterGroup> {
const viewFilterGroup = await this.findById(id, workspaceId);
if (!isDefined(viewFilterGroup)) {
throw new ViewFilterGroupException(
generateViewFilterGroupExceptionMessage(
ViewFilterGroupExceptionMessageKey.VIEW_FILTER_GROUP_NOT_FOUND,
id,
),
ViewFilterGroupExceptionCode.VIEW_FILTER_GROUP_NOT_FOUND,
);
}
await this.viewFilterGroupRepository.softDelete(id);
return viewFilterGroup;
}
}
@@ -0,0 +1,152 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { ViewFilter } from 'src/engine/core-modules/view/entities/view-filter.entity';
import {
ViewFilterException,
ViewFilterExceptionCode,
ViewFilterExceptionMessageKey,
generateViewFilterExceptionMessage,
generateViewFilterUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-filter.exception';
@Injectable()
export class ViewFilterService {
constructor(
@InjectRepository(ViewFilter, 'core')
private readonly viewFilterRepository: Repository<ViewFilter>,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewFilter[]> {
return this.viewFilterRepository.find({
where: {
workspaceId,
deletedAt: IsNull(),
},
order: { positionInViewFilterGroup: 'ASC' },
relations: ['workspace', 'view', 'viewFilterGroup'],
});
}
async findByViewId(
workspaceId: string,
viewId: string,
): Promise<ViewFilter[]> {
return this.viewFilterRepository.find({
where: {
workspaceId,
viewId,
deletedAt: IsNull(),
},
order: { positionInViewFilterGroup: 'ASC' },
relations: ['workspace', 'view', 'viewFilterGroup'],
});
}
async findById(id: string, workspaceId: string): Promise<ViewFilter | null> {
const viewFilter = await this.viewFilterRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
relations: ['workspace', 'view', 'viewFilterGroup'],
});
return viewFilter || null;
}
async create(viewFilterData: Partial<ViewFilter>): Promise<ViewFilter> {
if (!isDefined(viewFilterData.workspaceId)) {
throw new ViewFilterException(
generateViewFilterExceptionMessage(
ViewFilterExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
{
userFriendlyMessage: generateViewFilterUserFriendlyExceptionMessage(
ViewFilterExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
);
}
if (!isDefined(viewFilterData.viewId)) {
throw new ViewFilterException(
generateViewFilterExceptionMessage(
ViewFilterExceptionMessageKey.VIEW_ID_REQUIRED,
),
ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
{
userFriendlyMessage: generateViewFilterUserFriendlyExceptionMessage(
ViewFilterExceptionMessageKey.VIEW_ID_REQUIRED,
),
},
);
}
if (!isDefined(viewFilterData.fieldMetadataId)) {
throw new ViewFilterException(
generateViewFilterExceptionMessage(
ViewFilterExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
{
userFriendlyMessage: generateViewFilterUserFriendlyExceptionMessage(
ViewFilterExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
},
);
}
const viewFilter = this.viewFilterRepository.create(viewFilterData);
return this.viewFilterRepository.save(viewFilter);
}
async update(
id: string,
workspaceId: string,
updateData: Partial<ViewFilter>,
): Promise<ViewFilter> {
const existingViewFilter = await this.findById(id, workspaceId);
if (!isDefined(existingViewFilter)) {
throw new ViewFilterException(
generateViewFilterExceptionMessage(
ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND,
id,
),
ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND,
);
}
const updatedViewFilter = await this.viewFilterRepository.save({
id,
...updateData,
});
return { ...existingViewFilter, ...updatedViewFilter };
}
async delete(id: string, workspaceId: string): Promise<ViewFilter> {
const viewFilter = await this.findById(id, workspaceId);
if (!isDefined(viewFilter)) {
throw new ViewFilterException(
generateViewFilterExceptionMessage(
ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND,
id,
),
ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND,
);
}
await this.viewFilterRepository.softDelete(id);
return viewFilter;
}
}
@@ -0,0 +1,152 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { ViewGroup } from 'src/engine/core-modules/view/entities/view-group.entity';
import {
ViewGroupException,
ViewGroupExceptionCode,
ViewGroupExceptionMessageKey,
generateViewGroupExceptionMessage,
generateViewGroupUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-group.exception';
@Injectable()
export class ViewGroupService {
constructor(
@InjectRepository(ViewGroup, 'core')
private readonly viewGroupRepository: Repository<ViewGroup>,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewGroup[]> {
return this.viewGroupRepository.find({
where: {
workspaceId,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
}
async findByViewId(
workspaceId: string,
viewId: string,
): Promise<ViewGroup[]> {
return this.viewGroupRepository.find({
where: {
workspaceId,
viewId,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
}
async findById(id: string, workspaceId: string): Promise<ViewGroup | null> {
const viewGroup = await this.viewGroupRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
relations: ['workspace', 'view'],
});
return viewGroup || null;
}
async create(viewGroupData: Partial<ViewGroup>): Promise<ViewGroup> {
if (!isDefined(viewGroupData.workspaceId)) {
throw new ViewGroupException(
generateViewGroupExceptionMessage(
ViewGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA,
{
userFriendlyMessage: generateViewGroupUserFriendlyExceptionMessage(
ViewGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
);
}
if (!isDefined(viewGroupData.viewId)) {
throw new ViewGroupException(
generateViewGroupExceptionMessage(
ViewGroupExceptionMessageKey.VIEW_ID_REQUIRED,
),
ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA,
{
userFriendlyMessage: generateViewGroupUserFriendlyExceptionMessage(
ViewGroupExceptionMessageKey.VIEW_ID_REQUIRED,
),
},
);
}
if (!isDefined(viewGroupData.fieldMetadataId)) {
throw new ViewGroupException(
generateViewGroupExceptionMessage(
ViewGroupExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA,
{
userFriendlyMessage: generateViewGroupUserFriendlyExceptionMessage(
ViewGroupExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
},
);
}
const viewGroup = this.viewGroupRepository.create(viewGroupData);
return this.viewGroupRepository.save(viewGroup);
}
async update(
id: string,
workspaceId: string,
updateData: Partial<ViewGroup>,
): Promise<ViewGroup> {
const existingViewGroup = await this.findById(id, workspaceId);
if (!isDefined(existingViewGroup)) {
throw new ViewGroupException(
generateViewGroupExceptionMessage(
ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND,
id,
),
ViewGroupExceptionCode.VIEW_GROUP_NOT_FOUND,
);
}
const updatedViewGroup = await this.viewGroupRepository.save({
id,
...updateData,
});
return { ...existingViewGroup, ...updatedViewGroup };
}
async delete(id: string, workspaceId: string): Promise<ViewGroup> {
const viewGroup = await this.findById(id, workspaceId);
if (!isDefined(viewGroup)) {
throw new ViewGroupException(
generateViewGroupExceptionMessage(
ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND,
id,
),
ViewGroupExceptionCode.VIEW_GROUP_NOT_FOUND,
);
}
await this.viewGroupRepository.softDelete(id);
return viewGroup;
}
}
@@ -0,0 +1,147 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { ViewSort } from 'src/engine/core-modules/view/entities/view-sort.entity';
import {
ViewSortException,
ViewSortExceptionCode,
ViewSortExceptionMessageKey,
generateViewSortExceptionMessage,
generateViewSortUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-sort.exception';
@Injectable()
export class ViewSortService {
constructor(
@InjectRepository(ViewSort, 'core')
private readonly viewSortRepository: Repository<ViewSort>,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewSort[]> {
return this.viewSortRepository.find({
where: {
workspaceId,
deletedAt: IsNull(),
},
relations: ['workspace', 'view'],
});
}
async findByViewId(workspaceId: string, viewId: string): Promise<ViewSort[]> {
return this.viewSortRepository.find({
where: {
workspaceId,
viewId,
deletedAt: IsNull(),
},
relations: ['workspace', 'view'],
});
}
async findById(id: string, workspaceId: string): Promise<ViewSort | null> {
const viewSort = await this.viewSortRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
relations: ['workspace', 'view'],
});
return viewSort || null;
}
async create(viewSortData: Partial<ViewSort>): Promise<ViewSort> {
if (!isDefined(viewSortData.workspaceId)) {
throw new ViewSortException(
generateViewSortExceptionMessage(
ViewSortExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewSortExceptionCode.INVALID_VIEW_SORT_DATA,
{
userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage(
ViewSortExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
);
}
if (!isDefined(viewSortData.viewId)) {
throw new ViewSortException(
generateViewSortExceptionMessage(
ViewSortExceptionMessageKey.VIEW_ID_REQUIRED,
),
ViewSortExceptionCode.INVALID_VIEW_SORT_DATA,
{
userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage(
ViewSortExceptionMessageKey.VIEW_ID_REQUIRED,
),
},
);
}
if (!isDefined(viewSortData.fieldMetadataId)) {
throw new ViewSortException(
generateViewSortExceptionMessage(
ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
ViewSortExceptionCode.INVALID_VIEW_SORT_DATA,
{
userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage(
ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
},
);
}
const viewSort = this.viewSortRepository.create(viewSortData);
return this.viewSortRepository.save(viewSort);
}
async update(
id: string,
workspaceId: string,
updateData: Partial<ViewSort>,
): Promise<ViewSort> {
const existingViewSort = await this.findById(id, workspaceId);
if (!isDefined(existingViewSort)) {
throw new ViewSortException(
generateViewSortExceptionMessage(
ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND,
id,
),
ViewSortExceptionCode.VIEW_SORT_NOT_FOUND,
);
}
const updatedViewSort = await this.viewSortRepository.save({
id,
...updateData,
});
return { ...existingViewSort, ...updatedViewSort };
}
async delete(id: string, workspaceId: string): Promise<ViewSort> {
const viewSort = await this.findById(id, workspaceId);
if (!isDefined(viewSort)) {
throw new ViewSortException(
generateViewSortExceptionMessage(
ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND,
id,
),
ViewSortExceptionCode.VIEW_SORT_NOT_FOUND,
);
}
await this.viewSortRepository.softDelete(id);
return viewSort;
}
}
@@ -0,0 +1,159 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { View } from 'src/engine/core-modules/view/entities/view.entity';
import {
ViewException,
ViewExceptionCode,
ViewExceptionMessageKey,
generateViewExceptionMessage,
generateViewUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view.exception';
@Injectable()
export class ViewService {
constructor(
@InjectRepository(View, 'core')
private readonly viewRepository: Repository<View>,
) {}
async findByWorkspaceId(workspaceId: string): Promise<View[]> {
return this.viewRepository.find({
where: {
workspaceId,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
relations: [
'workspace',
'viewFields',
'viewFilters',
'viewSorts',
'viewGroups',
'viewFilterGroups',
],
});
}
async findByObjectMetadataId(
workspaceId: string,
objectMetadataId: string,
): Promise<View[]> {
return this.viewRepository.find({
where: {
workspaceId,
objectMetadataId,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
relations: [
'workspace',
'viewFields',
'viewFilters',
'viewSorts',
'viewGroups',
'viewFilterGroups',
],
});
}
async findById(id: string, workspaceId: string): Promise<View | null> {
const view = await this.viewRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
relations: [
'workspace',
'viewFields',
'viewFilters',
'viewSorts',
'viewGroups',
'viewFilterGroups',
],
});
return view || null;
}
async create(viewData: Partial<View>): Promise<View> {
if (!isDefined(viewData.workspaceId)) {
throw new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewExceptionCode.INVALID_VIEW_DATA,
{
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
ViewExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
);
}
if (!isDefined(viewData.objectMetadataId)) {
throw new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.OBJECT_METADATA_ID_REQUIRED,
),
ViewExceptionCode.INVALID_VIEW_DATA,
{
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
ViewExceptionMessageKey.OBJECT_METADATA_ID_REQUIRED,
),
},
);
}
const view = this.viewRepository.create(viewData);
return this.viewRepository.save(view);
}
async update(
id: string,
workspaceId: string,
updateData: Partial<View>,
): Promise<View> {
const existingView = await this.findById(id, workspaceId);
if (!isDefined(existingView)) {
throw new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.VIEW_NOT_FOUND,
id,
),
ViewExceptionCode.VIEW_NOT_FOUND,
);
}
const updatedView = await this.viewRepository.save({
id,
...updateData,
});
return { ...existingView, ...updatedView };
}
async delete(id: string, workspaceId: string): Promise<View> {
const view = await this.findById(id, workspaceId);
if (!isDefined(view)) {
throw new ViewException(
generateViewExceptionMessage(
ViewExceptionMessageKey.VIEW_NOT_FOUND,
id,
),
ViewExceptionCode.VIEW_NOT_FOUND,
);
}
await this.viewRepository.softDelete(id);
return view;
}
}
@@ -0,0 +1,12 @@
export type RelationFilterValue = {
isCurrentWorkspaceMemberSelected?: boolean;
selectedRecordIds: string[];
};
export type ViewFilterValue =
| string
| string[]
| RelationFilterValue
| Record<string, unknown>
| null
| undefined;
@@ -0,0 +1,128 @@
import {
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
ViewFieldException,
ViewFieldExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-field.exception';
import {
ViewFilterGroupException,
ViewFilterGroupExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-filter-group.exception';
import {
ViewFilterException,
ViewFilterExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-filter.exception';
import {
ViewGroupException,
ViewGroupExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-group.exception';
import {
ViewSortException,
ViewSortExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view-sort.exception';
import {
ViewException,
ViewExceptionCode,
} from 'src/engine/core-modules/view/exceptions/view.exception';
export const viewGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof ViewException) {
switch (error.code) {
case ViewExceptionCode.VIEW_NOT_FOUND:
throw new NotFoundError(error.message);
case ViewExceptionCode.INVALID_VIEW_DATA:
throw new UserInputError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
if (error instanceof ViewFieldException) {
switch (error.code) {
case ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND:
throw new NotFoundError(error.message);
case ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA:
throw new UserInputError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
if (error instanceof ViewFilterException) {
switch (error.code) {
case ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND:
throw new NotFoundError(error.message);
case ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA:
throw new UserInputError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
if (error instanceof ViewFilterGroupException) {
switch (error.code) {
case ViewFilterGroupExceptionCode.VIEW_FILTER_GROUP_NOT_FOUND:
throw new NotFoundError(error.message);
case ViewFilterGroupExceptionCode.INVALID_VIEW_FILTER_GROUP_DATA:
throw new UserInputError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
if (error instanceof ViewGroupException) {
switch (error.code) {
case ViewGroupExceptionCode.VIEW_GROUP_NOT_FOUND:
throw new NotFoundError(error.message);
case ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA:
throw new UserInputError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
if (error instanceof ViewSortException) {
switch (error.code) {
case ViewSortExceptionCode.VIEW_SORT_NOT_FOUND:
throw new NotFoundError(error.message);
case ViewSortExceptionCode.INVALID_VIEW_SORT_DATA:
throw new UserInputError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
throw error;
};
@@ -0,0 +1,31 @@
import { Catch, ExceptionFilter } from '@nestjs/common';
import { ViewFieldException } from 'src/engine/core-modules/view/exceptions/view-field.exception';
import { ViewFilterGroupException } from 'src/engine/core-modules/view/exceptions/view-filter-group.exception';
import { ViewFilterException } from 'src/engine/core-modules/view/exceptions/view-filter.exception';
import { ViewGroupException } from 'src/engine/core-modules/view/exceptions/view-group.exception';
import { ViewSortException } from 'src/engine/core-modules/view/exceptions/view-sort.exception';
import { ViewException } from 'src/engine/core-modules/view/exceptions/view.exception';
import { viewGraphqlApiExceptionHandler } from 'src/engine/core-modules/view/utils/view-graphql-api-exception-handler.util';
@Catch(
ViewException,
ViewFieldException,
ViewFilterException,
ViewFilterGroupException,
ViewGroupException,
ViewSortException,
)
export class ViewGraphqlApiExceptionFilter implements ExceptionFilter {
catch(
exception:
| ViewException
| ViewFieldException
| ViewFilterException
| ViewFilterGroupException
| ViewGroupException
| ViewSortException,
) {
return viewGraphqlApiExceptionHandler(exception);
}
}
@@ -0,0 +1,71 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { ViewFieldController } from 'src/engine/core-modules/view/controllers/view-field.controller';
import { ViewFilterGroupController } from 'src/engine/core-modules/view/controllers/view-filter-group.controller';
import { ViewFilterController } from 'src/engine/core-modules/view/controllers/view-filter.controller';
import { ViewGroupController } from 'src/engine/core-modules/view/controllers/view-group.controller';
import { ViewSortController } from 'src/engine/core-modules/view/controllers/view-sort.controller';
import { ViewController } from 'src/engine/core-modules/view/controllers/view.controller';
import { ViewField } from 'src/engine/core-modules/view/entities/view-field.entity';
import { ViewFilterGroup } from 'src/engine/core-modules/view/entities/view-filter-group.entity';
import { ViewFilter } from 'src/engine/core-modules/view/entities/view-filter.entity';
import { ViewGroup } from 'src/engine/core-modules/view/entities/view-group.entity';
import { ViewSort } from 'src/engine/core-modules/view/entities/view-sort.entity';
import { View } from 'src/engine/core-modules/view/entities/view.entity';
import { ViewFieldResolver } from 'src/engine/core-modules/view/resolvers/view-field.resolver';
import { ViewFilterGroupResolver } from 'src/engine/core-modules/view/resolvers/view-filter-group.resolver';
import { ViewFilterResolver } from 'src/engine/core-modules/view/resolvers/view-filter.resolver';
import { ViewGroupResolver } from 'src/engine/core-modules/view/resolvers/view-group.resolver';
import { ViewSortResolver } from 'src/engine/core-modules/view/resolvers/view-sort.resolver';
import { ViewResolver } from 'src/engine/core-modules/view/resolvers/view.resolver';
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service';
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@Module({
imports: [
TypeOrmModule.forFeature(
[View, ViewField, ViewFilter, ViewFilterGroup, ViewGroup, ViewSort],
'core',
),
AuthModule,
WorkspaceCacheStorageModule,
],
controllers: [
ViewController,
ViewFieldController,
ViewFilterController,
ViewFilterGroupController,
ViewGroupController,
ViewSortController,
],
providers: [
ViewService,
ViewFieldService,
ViewFilterService,
ViewFilterGroupService,
ViewGroupService,
ViewSortService,
ViewResolver,
ViewFieldResolver,
ViewFilterResolver,
ViewFilterGroupResolver,
ViewGroupResolver,
ViewSortResolver,
],
exports: [
ViewService,
ViewFieldService,
ViewFilterService,
ViewFilterGroupService,
ViewGroupService,
ViewSortService,
],
})
export class CoreViewModule {}