issue type
Gitea Actions / Deploy (push) Successful in 32s

This commit is contained in:
vadim committed 2026-10-18 20:37:58 +03:00
1 parent afc37f8fdb
commit c5691da2b8
47 files changed
+278 -9

No files matched your search

@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "issue" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'TASK';
-- CreateIndex
CREATE INDEX "issue_projectId_type_idx" ON "issue"("projectId", "type");
+2
View File
@@ -88,6 +88,7 @@ model Issue {
content String?
status String
priority Int @default(0)
type String @default("TASK")
assigneeId String?
assignee User? @relation(name: "assigneeIssues", fields: [assigneeId], references: [id], onDelete: SetNull)
reporterId String?
@@ -109,6 +110,7 @@ model Issue {
@@index([projectId, createdAt])
@@index([dueDate])
@@index([projectId, dueDate])
@@index([projectId, type])
@@map("issue")
}
+2
View File
@@ -5,6 +5,7 @@ import { IssueModule } from './modules/issue/issue.module.js';
import { ProjectModule } from './modules/project/project.module.js';
import { UserModule } from './modules/user/user.module.js';
import { StatusModule } from './modules/status/status.module.js';
import { IssueTypeModule } from './modules/issue-type/issue-type.module.js';
import { NotificationModule } from './modules/notification/notification.module.js';
import { CommentModule } from './modules/comment/comment.module.js';
import { FileModule } from './modules/file/file.module.js';
@@ -52,6 +53,7 @@ import { join } from 'node:path';
ProjectModule,
UserModule,
StatusModule,
IssueTypeModule,
NotificationModule,
CommentModule,
FileModule,
@@ -29,6 +29,7 @@ export const buildIssue = () =>
content: null,
status: 'OPEN',
priority: 0,
type: 'TASK',
assignee: null,
reporter: 'user-1',
projectId: 'project-1',
@@ -19,6 +19,7 @@ const buildIssue = () =>
content: null,
status: 'OPEN',
priority: 0,
type: 'TASK',
assignee: null,
reporter: 'user-1',
projectId: 'project-1',
@@ -0,0 +1,9 @@
import { Injectable } from '@nestjs/common';
import { ISSUE_TYPES } from '../../../../issue/domain/value-objects/issue-type.vo.js';
@Injectable()
export class ListIssueTypesUseCase {
execute(): typeof ISSUE_TYPES {
return ISSUE_TYPES;
}
}
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { IssueTypeController } from './presentation/controllers/issue-type.controller.js';
import { IssueTypeResolver } from './presentation/resolvers/issue-type.resolver.js';
import { ListIssueTypesUseCase } from './application/use-cases/list-issue-types/list-issue-types.use-case.js';
@Module({
controllers: [IssueTypeController],
providers: [ListIssueTypesUseCase, IssueTypeResolver],
})
export class IssueTypeModule {}
@@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common';
import { ListIssueTypesUseCase } from '../../application/use-cases/list-issue-types/list-issue-types.use-case.js';
import { ISSUE_TYPES } from '../../../issue/domain/value-objects/issue-type.vo.js';
@Controller('issue-types')
export class IssueTypeController {
constructor(private readonly listIssueTypesUseCase: ListIssueTypesUseCase) {}
@Get()
list(): typeof ISSUE_TYPES {
return this.listIssueTypesUseCase.execute();
}
}
@@ -0,0 +1,14 @@
import { Field, ObjectType } from '@nestjs/graphql';
/** Не `IssueType` — это имя занято value-объектом типа задачи */
@ObjectType('IssueTypeDefinition')
export class IssueTypeDefinitionType {
@Field()
name: string;
@Field()
alias: string;
@Field()
color: string;
}
@@ -0,0 +1,14 @@
import { Query, Resolver } from '@nestjs/graphql';
import { ListIssueTypesUseCase } from '../../application/use-cases/list-issue-types/list-issue-types.use-case.js';
import { ISSUE_TYPES } from '../../../issue/domain/value-objects/issue-type.vo.js';
import { IssueTypeDefinitionType } from '../graphql/issue-type-definition.type.js';
@Resolver()
export class IssueTypeResolver {
constructor(private readonly listIssueTypesUseCase: ListIssueTypesUseCase) {}
@Query(() => [IssueTypeDefinitionType], { name: 'issueTypes' })
list(): typeof ISSUE_TYPES {
return this.listIssueTypesUseCase.execute();
}
}
@@ -7,6 +7,7 @@ export class IssueWithUsersDto {
content: string | null;
status: string;
priority: number;
type: string;
assigneeId: string | null;
reporterId: string | null;
assignee: UserDto | null;
@@ -5,6 +5,7 @@ export class IssueDto {
content: string | null;
status: string;
priority: number;
type: string;
assigneeId: string | null;
reporterId: string | null;
projectId: string;
@@ -12,6 +12,7 @@ export class IssueApplicationMapper {
content: issue.content,
status: issue.status,
priority: issue.priority,
type: issue.type,
assigneeId: issue.assignee,
reporterId: issue.reporter,
projectId: issue.projectId,
@@ -30,6 +31,7 @@ export class IssueApplicationMapper {
content: issue.content,
status: issue.status,
priority: issue.priority,
type: issue.type,
assigneeId: issue.assignee,
reporterId: issue.reporter,
assignee: issue.assignee ? (usersById.get(issue.assignee) ?? null) : null,
@@ -24,6 +24,7 @@ const buildIssue = (overrides: Partial<{ assignee: string | null; reporter: stri
content: null,
status: 'OPEN',
priority: 0,
type: 'TASK',
assignee: 'assignee' in overrides ? (overrides.assignee ?? null) : 'user-2',
reporter: overrides.reporter ?? 'user-1',
projectId: 'project-1',
@@ -8,5 +8,6 @@ export class CreateIssueCommand {
public readonly priority?: number,
public readonly assignee?: string,
public readonly dueDate?: Date,
public readonly type?: string,
) {}
}
@@ -64,6 +64,7 @@ describe('CreateIssueUseCase', () => {
content: issue.content,
status: issue.status,
priority: issue.priority,
type: issue.type,
assignee: issue.assignee,
reporter: issue.reporter,
projectId: issue.projectId,
@@ -82,6 +83,7 @@ describe('CreateIssueUseCase', () => {
title: 'Fix login bug',
status: 'OPEN',
priority: 0,
type: 'TASK',
reporterId: 'user-1',
});
});
@@ -113,6 +115,7 @@ describe('CreateIssueUseCase', () => {
content: issue.content,
status: issue.status,
priority: issue.priority,
type: issue.type,
assignee: issue.assignee,
reporter: issue.reporter,
projectId: issue.projectId,
@@ -161,4 +164,26 @@ describe('CreateIssueUseCase', () => {
expect(notificationsService.notify).not.toHaveBeenCalled();
});
it('creates a TASK by default and accepts an explicit type', async () => {
vi.mocked(repository.create).mockImplementation(async (issue) => issue);
const task = await useCase.execute(new CreateIssueCommand('ISSUE-4', 'Something', 'user-1', 'project-1'));
const bug = await useCase.execute(
new CreateIssueCommand(
'ISSUE-5',
'Crash on start',
'user-1',
'project-1',
undefined,
undefined,
undefined,
undefined,
'BUG',
),
);
expect(task.type).toBe('TASK');
expect(bug.type).toBe('BUG');
});
});
@@ -30,6 +30,7 @@ export class CreateIssueUseCase {
title: command.title,
content: command.content,
priority: command.priority,
type: command.type,
assignee: command.assignee,
reporter: command.reporter,
projectId: command.projectId,
@@ -14,6 +14,7 @@ const buildIssue = () =>
content: null,
status: 'OPEN',
priority: 0,
type: 'TASK',
assignee: null,
reporter: 'user-1',
projectId: 'project-1',
@@ -15,6 +15,7 @@ const buildIssue = () =>
content: null,
status: 'OPEN',
priority: 0,
type: 'TASK',
assignee: 'user-2',
reporter: 'user-1',
projectId: 'project-1',
@@ -15,6 +15,7 @@ const buildIssue = () =>
content: null,
status: 'OPEN',
priority: 0,
type: 'TASK',
assignee: 'user-2',
reporter: 'user-1',
projectId: 'project-1',
@@ -18,6 +18,7 @@ const buildIssue = (id: number) =>
content: null,
status: 'OPEN',
priority: 0,
type: 'TASK',
assignee: null,
reporter: 'user-1',
projectId: 'project-1',
@@ -8,5 +8,6 @@ export class ListIssuesQuery {
public readonly text?: string,
public readonly assigneeId?: string,
public readonly hasDueDate?: boolean,
public readonly type?: string[],
) {}
}
@@ -14,6 +14,7 @@ const buildIssue = (id: number) =>
content: null,
status: 'OPEN',
priority: 0,
type: 'TASK',
assignee: null,
reporter: 'user-1',
projectId: 'project-1',
@@ -23,6 +23,7 @@ export class ListIssuesUseCase {
text: query.text,
assigneeId: query.assigneeId,
hasDueDate: query.hasDueDate,
type: query.type,
});
return { list: await this.issueUserExpander.expandAll(list), meta };
@@ -8,5 +8,6 @@ export class UpdateIssueCommand {
public readonly priority?: number,
public readonly assignee?: string | null,
public readonly dueDate?: Date | null,
public readonly type?: string,
) {}
}
@@ -19,6 +19,7 @@ const buildIssue = () =>
content: 'Original content',
status: 'OPEN',
priority: 0,
type: 'TASK',
assignee: null,
reporter: 'user-1',
projectId: 'project-1',
@@ -155,4 +156,14 @@ describe('UpdateIssueUseCase', () => {
expect(notificationsService.notify).not.toHaveBeenCalled();
});
it('changes the issue type', async () => {
vi.mocked(repository.findById).mockResolvedValue(buildIssue());
const dto = await useCase.execute(
new UpdateIssueCommand(1, 'user-1', undefined, undefined, undefined, undefined, undefined, undefined, 'BUG'),
);
expect(dto.type).toBe('BUG');
});
});
@@ -47,6 +47,10 @@ export class UpdateIssueUseCase {
issue.changePriority(command.priority);
}
if (command.type !== undefined) {
issue.changeType(command.type);
}
if (command.assignee !== undefined) {
if (command.assignee === null || command.assignee === '') {
issue.unassign();
@@ -8,6 +8,7 @@ export const ISSUE_CHANGE_FIELDS = [
'content',
'status',
'priority',
'type',
'assignee',
'dueDate',
] as const;
@@ -19,6 +19,7 @@ describe('Issue', () => {
expect(issue.content).toBeNull();
expect(issue.status).toBe('OPEN');
expect(issue.priority).toBe(0);
expect(issue.type).toBe('TASK');
expect(issue.assignee).toBeNull();
expect(issue.reporter).toBe('user-1');
});
@@ -31,11 +32,13 @@ describe('Issue', () => {
projectId: 'project-1',
content: 'Details here',
priority: 3,
type: 'BUG',
assignee: 'user-2',
});
expect(issue.content).toBe('Details here');
expect(issue.priority).toBe(3);
expect(issue.type).toBe('BUG');
expect(issue.assignee).toBe('user-2');
});
@@ -70,6 +73,7 @@ describe('Issue', () => {
content: null,
status: 'IN_PROGRESS',
priority: 2,
type: 'TASK',
assignee: 'user-3',
reporter: 'user-1',
projectId: 'project-1',
@@ -93,6 +97,7 @@ describe('Issue', () => {
content: null,
status: 'OPEN',
priority: 0,
type: 'TASK',
assignee: null,
reporter: 'user-1',
projectId: 'project-1',
@@ -139,6 +144,15 @@ describe('Issue', () => {
expect(() => issue.changePriority(10)).toThrow();
});
it('changes the type through the value object rules', () => {
const issue = build();
issue.changeType('FEATURE');
expect(issue.type).toBe('FEATURE');
expect(() => issue.changeType('EPIC')).toThrow();
});
it('assigns and unassigns an issue', () => {
const issue = build();
@@ -169,6 +183,7 @@ describe('Issue', () => {
content: null,
status: 'OPEN',
priority: 0,
type: 'TASK',
assignee: null,
reporter: 'user-1',
projectId: 'project-1',
@@ -193,6 +208,7 @@ describe('Issue', () => {
issue.updateContent('Text');
issue.changeStatus('IN_PROGRESS');
issue.changePriority(3);
issue.changeType('BUG');
issue.assignTo('user-2');
issue.updateDueDate(new Date('2026-02-01T00:00:00.000Z'));
@@ -201,6 +217,7 @@ describe('Issue', () => {
{ field: 'content', oldValue: null, newValue: 'Text' },
{ field: 'status', oldValue: 'OPEN', newValue: 'IN_PROGRESS' },
{ field: 'priority', oldValue: '0', newValue: '3' },
{ field: 'type', oldValue: 'TASK', newValue: 'BUG' },
{ field: 'assignee', oldValue: null, newValue: 'user-2' },
{ field: 'dueDate', oldValue: null, newValue: '2026-02-01T00:00:00.000Z' },
]);
@@ -1,6 +1,7 @@
import { IssueTitle } from '../value-objects/issue-title.vo.js';
import { IssueStatus } from '../value-objects/issue-status.vo.js';
import { IssuePriority } from '../value-objects/issue-priority.vo.js';
import { IssueType } from '../value-objects/issue-type.vo.js';
import { IssueCreatedEvent } from '../events/issue-created.event.js';
import { IssueUpdatedEvent } from '../events/issue-updated.event.js';
import { IssueAssignedEvent } from '../events/issue-assigned.event.js';
@@ -13,6 +14,7 @@ interface IssueProps {
content: string | null;
status: IssueStatus;
priority: IssuePriority;
type: IssueType;
assignee: string | null;
/** null — автор задачи удалил аккаунт */
reporter: string | null;
@@ -46,6 +48,7 @@ export class Issue {
title: string;
content?: string | null;
priority?: number;
type?: string;
assignee?: string | null;
reporter: string;
projectId: string;
@@ -58,6 +61,7 @@ export class Issue {
content: params.content ?? null,
status: IssueStatus.open(),
priority: params.priority !== undefined ? IssuePriority.create(params.priority) : IssuePriority.default(),
type: params.type !== undefined ? IssueType.create(params.type) : IssueType.default(),
assignee: params.assignee ?? null,
reporter: params.reporter,
projectId: params.projectId,
@@ -84,6 +88,7 @@ export class Issue {
content: string | null;
status: string;
priority: number;
type: string;
assignee: string | null;
reporter: string | null;
projectId: string;
@@ -98,6 +103,7 @@ export class Issue {
content: props.content,
status: IssueStatus.create(props.status),
priority: IssuePriority.create(props.priority),
type: IssueType.create(props.type),
assignee: props.assignee,
reporter: props.reporter,
projectId: props.projectId,
@@ -131,6 +137,10 @@ export class Issue {
return this.props.priority.value;
}
get type(): string {
return this.props.type.value;
}
get assignee(): string | null {
return this.props.assignee;
}
@@ -178,6 +188,12 @@ export class Issue {
this.props.priority = next;
}
changeType(type: string): void {
const next = IssueType.create(type);
this.recordChange('type', this.type, next.value);
this.props.type = next;
}
assignTo(userId: string): void {
if (this.assignee === userId) return;
@@ -17,6 +17,7 @@ export interface IIssueRepository {
assigneeId?: string;
projectId?: string;
hasDueDate?: boolean;
type?: string[];
}): Promise<{ list: Issue[]; meta: PaginationMeta }>;
/** Задачи с dueDate в [from, to), по возрастанию dueDate, не больше limit */
findByDueDateRange(params: {
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest';
import { IssueType, ISSUE_TYPES } from './issue-type.vo.js';
describe('IssueType', () => {
it.each(ISSUE_TYPES.map((type) => type.alias))('accepts the valid type "%s"', (alias) => {
expect(IssueType.create(alias).value).toBe(alias);
});
it('throws for an unknown type', () => {
expect(() => IssueType.create('EPIC')).toThrow(/Недопустимый тип задачи/);
});
it('defaults to TASK', () => {
expect(IssueType.default().value).toBe('TASK');
});
it('compares two types by value', () => {
expect(IssueType.create('BUG').equals(IssueType.create('BUG'))).toBe(true);
expect(IssueType.create('BUG').equals(IssueType.create('TASK'))).toBe(false);
});
});
@@ -0,0 +1,33 @@
export const ISSUE_TYPES = [
{ name: 'Task', alias: 'TASK', color: 'primary' },
{ name: 'Bug', alias: 'BUG', color: 'error' },
{ name: 'Feature', alias: 'FEATURE', color: 'success' },
] as const;
export type IssueTypeValue = (typeof ISSUE_TYPES)[number]['alias'];
export const ISSUE_TYPE_ALIASES: readonly IssueTypeValue[] = ISSUE_TYPES.map((type) => type.alias);
export class IssueType {
private constructor(private readonly _value: IssueTypeValue) {}
static create(value: string): IssueType {
if (!ISSUE_TYPE_ALIASES.includes(value as IssueTypeValue)) {
throw new Error(`Недопустимый тип задачи: "${value}". Разрешены: ${ISSUE_TYPE_ALIASES.join(', ')}`);
}
return new IssueType(value as IssueTypeValue);
}
static default(): IssueType {
return new IssueType('TASK');
}
get value(): IssueTypeValue {
return this._value;
}
equals(other: IssueType): boolean {
return this._value === other.value;
}
}
@@ -10,6 +10,7 @@ describe('IssuePrismaMapper', () => {
content: 'Details',
status: 'IN_PROGRESS',
priority: 2,
type: 'BUG',
assigneeId: 'user-2',
reporterId: 'user-1',
projectId: 'project-1',
@@ -26,6 +27,7 @@ describe('IssuePrismaMapper', () => {
expect(issue.title).toBe('Fix login bug');
expect(issue.status).toBe('IN_PROGRESS');
expect(issue.priority).toBe(2);
expect(issue.type).toBe('BUG');
expect(issue.assignee).toBe('user-2');
});
@@ -45,6 +47,7 @@ describe('IssuePrismaMapper', () => {
content: null,
status: 'OPEN',
priority: 0,
type: 'TASK',
assigneeId: null,
reporterId: 'user-1',
projectId: 'project-1',
@@ -61,6 +64,7 @@ describe('IssuePrismaMapper', () => {
title: 'Updated title',
status: 'IN_PROGRESS',
priority: 2,
type: 'BUG',
assigneeId: 'user-2',
});
expect(input).not.toHaveProperty('id');
@@ -11,6 +11,7 @@ export class IssuePrismaMapper {
content: record.content,
status: record.status,
priority: record.priority,
type: record.type,
assignee: record.assigneeId,
reporter: record.reporterId,
projectId: record.projectId,
@@ -27,6 +28,7 @@ export class IssuePrismaMapper {
content: issue.content,
status: issue.status,
priority: issue.priority,
type: issue.type,
assigneeId: issue.assignee,
reporterId: issue.reporter,
projectId: issue.projectId,
@@ -42,6 +44,7 @@ export class IssuePrismaMapper {
content: issue.content,
status: issue.status,
priority: issue.priority,
type: issue.type,
assigneeId: issue.assignee,
dueDate: issue.dueDate,
updatedAt: issue.updatedAt,
@@ -73,6 +73,7 @@ export class IssuePrismaRepository implements IIssueRepository {
assigneeId?: string;
projectId?: string;
hasDueDate?: boolean;
type?: string[];
}): Promise<{ list: Issue[]; meta: PaginationMeta }> {
const page = Math.max(1, params?.page);
const limit = Math.max(1, params?.limit);
@@ -81,6 +82,7 @@ export class IssuePrismaRepository implements IIssueRepository {
where: {
AND: [
...commonFilters(params),
...(params.type?.length ? [{ type: { in: params.type } }] : []),
...(params.hasDueDate !== undefined ? [{ dueDate: params.hasDueDate ? { not: null } : null }] : []),
...(params.text
? [
@@ -70,6 +70,7 @@ export class IssueController {
dto.priority,
dto.assignee,
dto.dueDate !== undefined ? new Date(dto.dueDate) : undefined,
dto.type,
),
),
);
@@ -87,6 +88,7 @@ export class IssueController {
query.text,
query.assigneeId,
query.hasDueDate,
query.type,
),
);
}
@@ -167,6 +169,7 @@ export class IssueController {
dto.priority,
dto.assignee,
dto.dueDate === undefined ? undefined : dto.dueDate === null ? null : new Date(dto.dueDate),
dto.type,
),
),
);
@@ -1,6 +1,6 @@
import { IsISO8601, Matches } from 'class-validator';
import { StringDecorator } from '../../../../common/decorators/validators.js';
import { IssueStatusListDecorator } from './issue-status-list.decorator.js';
import { IssueStatusListDecorator } from './issue-alias-list.decorator.js';
/** Без смещения дата трактовалась бы в часовом поясе сервера */
const WITH_OFFSET = /(Z|[+-]\d{2}:\d{2})$/;
@@ -1,4 +1,5 @@
import { IsDateString, IsInt, IsNotEmpty, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
import { IsDateString, IsIn, IsInt, IsNotEmpty, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
import { ISSUE_TYPE_ALIASES } from '../../domain/value-objects/issue-type.vo.js';
export class CreateIssueRequestDto {
@IsString()
@@ -24,6 +25,10 @@ export class CreateIssueRequestDto {
@Max(4)
priority?: number;
@IsOptional()
@IsIn(ISSUE_TYPE_ALIASES)
type?: string;
@IsOptional()
@IsString()
assignee?: string;
@@ -1,5 +1,5 @@
import { BooleanDecorator, IntDecorator, StringDecorator } from '../../../../common/decorators/validators.js';
import { IssueStatusListDecorator } from './issue-status-list.decorator.js';
import { IssueStatusListDecorator, IssueTypeListDecorator } from './issue-alias-list.decorator.js';
export class FilterIssueDto {
@IntDecorator({ isOptional: true })
@@ -17,6 +17,9 @@ export class FilterIssueDto {
@IssueStatusListDecorator()
status?: string[];
@IssueTypeListDecorator()
type?: string[];
@StringDecorator({ isOptional: true })
projectId?: string;
@@ -2,9 +2,10 @@ import { applyDecorators } from '@nestjs/common';
import { Transform } from 'class-transformer';
import { ArrayNotEmpty, IsArray, IsIn, IsOptional } from 'class-validator';
import { ISSUE_STATUSES } from '../../domain/value-objects/issue-status.vo.js';
import { ISSUE_TYPE_ALIASES } from '../../domain/value-objects/issue-type.vo.js';
/** Comma-separated (`?status=OPEN,IN_PROGRESS`) or repeated (`?status=OPEN&status=IN_PROGRESS`) */
export function IssueStatusListDecorator() {
function AliasListDecorator(aliases: readonly string[]) {
return applyDecorators(
IsOptional(),
Transform(({ value }) =>
@@ -18,9 +19,15 @@ export function IssueStatusListDecorator() {
),
IsArray(),
ArrayNotEmpty(),
IsIn(
ISSUE_STATUSES.map((status) => status.alias),
{ each: true },
),
IsIn(aliases, { each: true }),
);
}
export function IssueStatusListDecorator() {
return AliasListDecorator(ISSUE_STATUSES.map((status) => status.alias));
}
/** `?type=BUG,FEATURE` или `?type=BUG&type=FEATURE` */
export function IssueTypeListDecorator() {
return AliasListDecorator(ISSUE_TYPE_ALIASES);
}
@@ -1,5 +1,6 @@
import { IsDateString, IsIn, IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
import { ISSUE_STATUSES } from '../../domain/value-objects/issue-status.vo.js';
import { ISSUE_TYPE_ALIASES } from '../../domain/value-objects/issue-type.vo.js';
export class UpdateIssueRequestDto {
@IsOptional()
@@ -21,6 +22,10 @@ export class UpdateIssueRequestDto {
@Max(4)
priority?: number;
@IsOptional()
@IsIn(ISSUE_TYPE_ALIASES)
type?: string;
@IsOptional()
@IsString()
assignee?: string | null;
@@ -1,5 +1,6 @@
import { Field, InputType, Int } from '@nestjs/graphql';
import { IsDate, IsInt, IsNotEmpty, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
import { IsDate, IsIn, IsInt, IsNotEmpty, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
import { ISSUE_TYPE_ALIASES } from '../../domain/value-objects/issue-type.vo.js';
@InputType()
export class CreateIssueInput {
@@ -31,6 +32,11 @@ export class CreateIssueInput {
@Max(4)
priority?: number;
@Field({ nullable: true })
@IsOptional()
@IsIn(ISSUE_TYPE_ALIASES)
type?: string;
@Field({ nullable: true })
@IsOptional()
@IsString()
@@ -21,6 +21,9 @@ export class IssueDetailType {
@Field(() => Int)
priority: number;
@Field()
type: string;
@Field(() => String, { nullable: true })
assigneeId: string | null;
@@ -20,6 +20,9 @@ export class IssueType {
@Field(() => Int)
priority: number;
@Field()
type: string;
@Field(() => String, { nullable: true })
assigneeId: string | null;
@@ -1,6 +1,7 @@
import { Field, InputType, Int } from '@nestjs/graphql';
import { IsDate, IsIn, IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
import { ISSUE_STATUSES } from '../../domain/value-objects/issue-status.vo.js';
import { ISSUE_TYPE_ALIASES } from '../../domain/value-objects/issue-type.vo.js';
@InputType()
export class UpdateIssueInput {
@@ -27,6 +28,11 @@ export class UpdateIssueInput {
@Max(4)
priority?: number;
@Field({ nullable: true })
@IsOptional()
@IsIn(ISSUE_TYPE_ALIASES)
type?: string;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@@ -57,6 +57,7 @@ export class IssueResolver {
@Session() session: UserSession,
@Args('projectId', { type: () => String, nullable: true }) projectId?: string,
@Args('hasDueDate', { type: () => Boolean, nullable: true }) hasDueDate?: boolean,
@Args('type', { type: () => [String], nullable: true }) type?: string[],
): Promise<{ list: IssueWithUsersDto[]; meta: PaginationMeta }> {
return this.listIssuesUseCase.execute(
new ListIssuesQuery(
@@ -68,6 +69,7 @@ export class IssueResolver {
undefined,
undefined,
hasDueDate ?? undefined,
type ?? undefined,
),
);
}
@@ -102,6 +104,7 @@ export class IssueResolver {
input.priority,
input.assignee,
input.dueDate,
input.type,
),
),
);
@@ -124,6 +127,7 @@ export class IssueResolver {
input.priority,
input.assignee,
input.dueDate,
input.type,
),
),
);