Move view in metadata-modules/ and create atomic folder + module for each view entity (#14990)
# Introduction Preparing view-filter and view-group introduction in v2 core engine Moving view from `core-modules` to `metadata-modules` ## What happened ### Created dedicated modules for each view entity: - ViewFieldModule - ViewFilterModule - ViewFilterGroupModule - ViewGroupModule - ViewSortModule ### Each module is now completely independent with its own: - Controller - Resolver - Service - Entity ### Created dedicated abstraction metadata module folder for: - flat-view-field - flat-view ### Dependencies - Eleminated circular dep on ViewModule to all others ones - Granular import not importing the whole viewModule anymore everywhere close https://github.com/twentyhq/core-team-issues/issues/1703
This commit is contained in:
+112
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
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';
|
||||
import { CreateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input';
|
||||
import { UpdateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input';
|
||||
import { type ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto';
|
||||
import {
|
||||
ViewSortException,
|
||||
ViewSortExceptionCode,
|
||||
ViewSortExceptionMessageKey,
|
||||
generateViewSortExceptionMessage,
|
||||
generateViewSortUserFriendlyExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception';
|
||||
import { ViewSortRestApiExceptionFilter } from 'src/engine/metadata-modules/view-sort/filters/view-sort-rest-api-exception.filter';
|
||||
import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service';
|
||||
|
||||
@Controller('rest/metadata/viewSorts')
|
||||
@UseGuards(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> {
|
||||
const viewSort = await this.viewSortService.findById(id, workspace.id);
|
||||
|
||||
if (!isDefined(viewSort)) {
|
||||
throw new ViewSortException(
|
||||
generateViewSortExceptionMessage(
|
||||
ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
ViewSortExceptionCode.VIEW_SORT_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage(
|
||||
ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return viewSort;
|
||||
}
|
||||
|
||||
@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) };
|
||||
}
|
||||
|
||||
// TODO: the destroy endpoint will be implemented when we settle on a strategy
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction';
|
||||
|
||||
@InputType()
|
||||
export class CreateViewSortInput {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
id?: string;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
fieldMetadataId: string;
|
||||
|
||||
@Field(() => ViewSortDirection, {
|
||||
nullable: true,
|
||||
defaultValue: ViewSortDirection.ASC,
|
||||
})
|
||||
direction?: ViewSortDirection;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
viewId: string;
|
||||
}
|
||||
+6
@@ -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,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/metadata-modules/view-sort/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;
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface';
|
||||
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
|
||||
@Entity({ name: 'viewSort', schema: 'core' })
|
||||
@Index('IDX_VIEW_SORT_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
|
||||
@Index('IDX_VIEW_SORT_VIEW_ID', ['viewId'], {
|
||||
where: '"deletedAt" IS NULL',
|
||||
})
|
||||
@Index(
|
||||
'IDX_VIEW_SORT_FIELD_METADATA_ID_VIEW_ID_UNIQUE',
|
||||
['fieldMetadataId', 'viewId'],
|
||||
{
|
||||
unique: true,
|
||||
where: '"deletedAt" IS NULL',
|
||||
},
|
||||
)
|
||||
export class ViewSortEntity extends SyncableEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
fieldMetadataId: string;
|
||||
|
||||
@ManyToOne(() => FieldMetadataEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'fieldMetadataId' })
|
||||
fieldMetadata: Relation<FieldMetadataEntity>;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'enum',
|
||||
enum: Object.values(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(() => ViewEntity, (view) => view.viewSorts, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'viewId' })
|
||||
view: Relation<ViewEntity>;
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export enum ViewSortDirection {
|
||||
ASC = 'ASC',
|
||||
DESC = 'DESC',
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
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 = t`WorkspaceId is required`;
|
||||
break;
|
||||
case ViewSortExceptionMessageKey.VIEW_ID_REQUIRED:
|
||||
message = t`ViewId is required`;
|
||||
break;
|
||||
case ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND:
|
||||
message = id
|
||||
? t`View sort (id: ${id}) not found`
|
||||
: t`View sort not found`;
|
||||
break;
|
||||
case ViewSortExceptionMessageKey.INVALID_VIEW_SORT_DATA:
|
||||
message = id
|
||||
? t`Invalid view sort data for view sort id: ${id}`
|
||||
: t`Invalid view sort data`;
|
||||
break;
|
||||
case ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
|
||||
message = t`FieldMetadataId is required`;
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(key);
|
||||
}
|
||||
|
||||
return 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.`;
|
||||
}
|
||||
};
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
type ArgumentsHost,
|
||||
Catch,
|
||||
type ExceptionFilter,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Response } from 'express';
|
||||
|
||||
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
|
||||
import {
|
||||
ViewSortException,
|
||||
ViewSortExceptionCode,
|
||||
} from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception';
|
||||
import { type 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
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';
|
||||
import { CreateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input';
|
||||
import { UpdateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input';
|
||||
import { ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto';
|
||||
import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service';
|
||||
import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter';
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async destroyCoreViewSort(
|
||||
@Args('id', { type: () => String }) id: string,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<boolean> {
|
||||
const deletedViewSort = await this.viewSortService.destroy(
|
||||
id,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return isDefined(deletedViewSort);
|
||||
}
|
||||
}
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction';
|
||||
import {
|
||||
ViewSortException,
|
||||
ViewSortExceptionCode,
|
||||
ViewSortExceptionMessageKey,
|
||||
generateViewSortExceptionMessage,
|
||||
generateViewSortUserFriendlyExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception';
|
||||
import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
describe('ViewSortService', () => {
|
||||
let viewSortService: ViewSortService;
|
||||
let viewSortRepository: Repository<ViewSortEntity>;
|
||||
|
||||
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 ViewSortEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ViewSortService,
|
||||
{
|
||||
provide: getRepositoryToken(ViewSortEntity),
|
||||
useValue: {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceCacheStorageService,
|
||||
useValue: {
|
||||
flushGraphQLOperation: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
viewSortService = module.get<ViewSortService>(ViewSortService);
|
||||
viewSortRepository = module.get<Repository<ViewSortEntity>>(
|
||||
getRepositoryToken(ViewSortEntity),
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
it('should destroy a view sort successfully', async () => {
|
||||
const id = 'view-sort-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(viewSortService, 'findById').mockResolvedValue(mockViewSort);
|
||||
jest.spyOn(viewSortRepository, 'delete').mockResolvedValue({} as any);
|
||||
|
||||
const result = await viewSortService.destroy(id, workspaceId);
|
||||
|
||||
expect(viewSortService.findById).toHaveBeenCalledWith(id, workspaceId);
|
||||
expect(viewSortRepository.delete).toHaveBeenCalledWith(id);
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import {
|
||||
ViewSortException,
|
||||
ViewSortExceptionCode,
|
||||
ViewSortExceptionMessageKey,
|
||||
generateViewSortExceptionMessage,
|
||||
generateViewSortUserFriendlyExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception';
|
||||
import { FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION } from 'src/engine/metadata-modules/view/constants/find-all-core-views-graphql-operation.constant';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
@Injectable()
|
||||
export class ViewSortService {
|
||||
constructor(
|
||||
@InjectRepository(ViewSortEntity)
|
||||
private readonly viewSortRepository: Repository<ViewSortEntity>,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
) {}
|
||||
|
||||
async findByWorkspaceId(workspaceId: string): Promise<ViewSortEntity[]> {
|
||||
return this.viewSortRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
}
|
||||
|
||||
async findByViewId(
|
||||
workspaceId: string,
|
||||
viewId: string,
|
||||
): Promise<ViewSortEntity[]> {
|
||||
return this.viewSortRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
viewId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<ViewSortEntity | null> {
|
||||
const viewSort = await this.viewSortRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['workspace', 'view'],
|
||||
});
|
||||
|
||||
return viewSort || null;
|
||||
}
|
||||
|
||||
async create(viewSortData: Partial<ViewSortEntity>): Promise<ViewSortEntity> {
|
||||
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);
|
||||
|
||||
const savedViewSort = await this.viewSortRepository.save(viewSort);
|
||||
|
||||
await this.flushGraphQLCache(viewSortData.workspaceId);
|
||||
|
||||
return savedViewSort;
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
updateData: Partial<ViewSortEntity>,
|
||||
): Promise<ViewSortEntity> {
|
||||
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,
|
||||
});
|
||||
|
||||
await this.flushGraphQLCache(workspaceId);
|
||||
|
||||
return { ...existingViewSort, ...updatedViewSort };
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<ViewSortEntity> {
|
||||
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);
|
||||
|
||||
await this.flushGraphQLCache(workspaceId);
|
||||
|
||||
return viewSort;
|
||||
}
|
||||
|
||||
async destroy(id: string, workspaceId: string): Promise<boolean> {
|
||||
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.delete(id);
|
||||
|
||||
await this.flushGraphQLCache(workspaceId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async flushGraphQLCache(workspaceId: string): Promise<void> {
|
||||
await this.workspaceCacheStorageService.flushGraphQLOperation({
|
||||
operationName: FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ViewSortController } from 'src/engine/metadata-modules/view-sort/controllers/view-sort.controller';
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { ViewSortResolver } from 'src/engine/metadata-modules/view-sort/resolvers/view-sort.resolver';
|
||||
import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ViewSortEntity]),
|
||||
WorkspaceCacheStorageModule,
|
||||
],
|
||||
controllers: [ViewSortController],
|
||||
providers: [ViewSortService, ViewSortResolver],
|
||||
exports: [ViewSortService],
|
||||
})
|
||||
export class ViewSortModule {}
|
||||
Reference in New Issue
Block a user