Owners and admins turn on a secret URL per project (Settings →
Integrations), POST /api/webhooks/alerts/{project}/{token}, that Sentry,
Uptime Kuma, Prometheus Alertmanager, Grafana or any tool sending JSON
with a title can call. A new problem opens a bug as TaskStream (label
«alerta», chosen type and priority); while that bug is open, the same
fingerprint goes to it (a comment at most every 30 minutes) and
«resolved» is said on it; once it is closed, a new bug names the old
one. Alerts of a project are handled one at a time, so two at once open
one bug, and a bug not yet projected still counts as open. Unknown
project, alerts off or a bad token all answer 403.
TaskStream may now comment as the system actor, like it already opened
tickets for public proposals. Stored in Mongo (alert_integrations,
alert_traces), not in the event store.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1591 lines
37 KiB
TypeScript
1591 lines
37 KiB
TypeScript
import {
|
|
ApiProperty,
|
|
ApiPropertyOptional,
|
|
OmitType,
|
|
PartialType,
|
|
} from '@nestjs/swagger';
|
|
import {
|
|
CustomFieldType,
|
|
IssueLinkType,
|
|
IssuePriority,
|
|
ProjectRole,
|
|
REACTIONS,
|
|
SETTING_COLORS,
|
|
StatusCategory,
|
|
TYPE_ICONS,
|
|
} from '@task-stream/api/tracker/domain';
|
|
import { IssueSort } from '@task-stream/api/tracker/application';
|
|
import { Transform, Type } from 'class-transformer';
|
|
import {
|
|
ArrayMaxSize,
|
|
IsArray,
|
|
IsBoolean,
|
|
IsEnum,
|
|
IsIn,
|
|
IsInt,
|
|
IsISO8601,
|
|
IsObject,
|
|
IsOptional,
|
|
IsUUID,
|
|
IsString,
|
|
IsUrl,
|
|
Matches,
|
|
Max,
|
|
MaxLength,
|
|
Min,
|
|
MinLength,
|
|
ValidateIf,
|
|
ValidateNested,
|
|
} from 'class-validator';
|
|
|
|
/* REST request bodies and query strings, validated like the GraphQL inputs. */
|
|
|
|
/** `?status=TODO&status=DONE` or `?status=TODO,DONE` → ['TODO', 'DONE']. */
|
|
const toList = ({ value }: { value: unknown }) =>
|
|
value === undefined || value === ''
|
|
? undefined
|
|
: ([] as unknown[])
|
|
.concat(value)
|
|
.flatMap((v) => String(v).split(','))
|
|
.map((v) => v.trim())
|
|
.filter(Boolean);
|
|
|
|
const toBoolean = ({ value }: { value: unknown }) =>
|
|
value === undefined ? undefined : value === true || value === 'true';
|
|
|
|
/** Ids of statuses, types and areas (checked against the project's settings). */
|
|
const SETTING_ID = /^[A-Za-z][A-Za-z0-9_]{0,29}$/;
|
|
|
|
// ---- Projects ---------------------------------------------------------------
|
|
|
|
export class CreateProjectDto {
|
|
@ApiProperty({
|
|
example: 'WEB',
|
|
description:
|
|
'2-10 letters/digits starting with a letter → tickets WEB-1, WEB-2…',
|
|
})
|
|
@Matches(/^[A-Za-z][A-Za-z0-9]{1,9}$/)
|
|
key!: string;
|
|
|
|
@ApiProperty({ example: 'Checkout revamp', maxLength: 120 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(120)
|
|
name!: string;
|
|
|
|
@ApiPropertyOptional({ type: String, nullable: true, maxLength: 5000 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(5000)
|
|
description?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
default: false,
|
|
description:
|
|
'Anyone can read it without signing in (board, tickets, comments, releases, activity, statistics)',
|
|
})
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
isPublic?: boolean;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
example: 'API',
|
|
description:
|
|
'A project you own or administer (id or key) to start from: its statuses, transitions, types, areas, custom fields and automation rules are copied',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
template?: string | null;
|
|
}
|
|
|
|
export class UpdateProjectDto extends PartialType(
|
|
OmitType(CreateProjectDto, ['key', 'template'] as const),
|
|
) {
|
|
@ApiPropertyOptional({
|
|
description:
|
|
'Take bug reports and ideas from anyone through the public form (while public)',
|
|
})
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
acceptsProposals?: boolean;
|
|
}
|
|
|
|
export class DeleteProjectQueryDto {
|
|
@ApiProperty({
|
|
example: 'WEB',
|
|
description: 'The project key, to confirm the deletion',
|
|
})
|
|
@IsString()
|
|
confirm!: string;
|
|
}
|
|
|
|
export class AddMemberDto {
|
|
@ApiProperty({ description: 'Pireon user id (see GET /users)' })
|
|
@IsString()
|
|
userId!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
enum: ProjectRole,
|
|
enumName: 'ProjectRole',
|
|
default: ProjectRole.MEMBER,
|
|
})
|
|
@IsOptional()
|
|
@IsEnum(ProjectRole)
|
|
role: ProjectRole = ProjectRole.MEMBER;
|
|
}
|
|
|
|
// ---- Project settings -------------------------------------------------------
|
|
|
|
export class WorkflowStatusBodyDto {
|
|
@ApiProperty({
|
|
example: 'IN_REVIEW',
|
|
description: 'Never change an existing id',
|
|
})
|
|
@Matches(SETTING_ID)
|
|
id!: string;
|
|
@ApiProperty({ example: 'En revisión', maxLength: 40 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(40)
|
|
name!: string;
|
|
@ApiProperty({ enum: StatusCategory, enumName: 'StatusCategory' })
|
|
@IsEnum(StatusCategory)
|
|
category!: StatusCategory;
|
|
@ApiProperty({ enum: SETTING_COLORS, enumName: 'SettingColor' })
|
|
@IsIn(SETTING_COLORS)
|
|
color!: string;
|
|
@ApiPropertyOptional({ type: Boolean, default: false })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
requiresReason = false;
|
|
@ApiPropertyOptional({ type: Boolean, default: false })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
archived = false;
|
|
@ApiPropertyOptional({
|
|
type: Number,
|
|
nullable: true,
|
|
minimum: 1,
|
|
maximum: 999,
|
|
description: 'Work-in-progress limit (null: none)',
|
|
})
|
|
@IsOptional()
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(999)
|
|
wipLimit?: number | null;
|
|
}
|
|
|
|
export class StatusTransitionsBodyDto {
|
|
@ApiProperty({ example: 'IN_QA' }) @Matches(SETTING_ID) from!: string;
|
|
@ApiProperty({ type: [String], example: ['DONE', 'QA_FAILED'] })
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@Matches(SETTING_ID, { each: true })
|
|
to!: string[];
|
|
}
|
|
|
|
export class IssueTemplateBodyDto {
|
|
@ApiPropertyOptional({ type: String, nullable: true, maxLength: 5000 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(5000)
|
|
description?: string | null;
|
|
@ApiPropertyOptional({ type: [String], maxItems: 20 })
|
|
@IsOptional()
|
|
@IsArray()
|
|
@ArrayMaxSize(20)
|
|
@IsString({ each: true })
|
|
@MaxLength(300, { each: true })
|
|
checklist?: string[];
|
|
}
|
|
|
|
export class IssueTypeOptionBodyDto {
|
|
@ApiProperty({ example: 'SPIKE' }) @Matches(SETTING_ID) id!: string;
|
|
@ApiProperty({ example: 'Spike', maxLength: 40 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(40)
|
|
name!: string;
|
|
@ApiProperty({ enum: TYPE_ICONS, enumName: 'TypeIcon' })
|
|
@IsIn(TYPE_ICONS)
|
|
icon!: string;
|
|
@ApiProperty({ enum: SETTING_COLORS, enumName: 'SettingColor' })
|
|
@IsIn(SETTING_COLORS)
|
|
color!: string;
|
|
@ApiPropertyOptional({ type: Boolean, default: false })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
archived = false;
|
|
@ApiPropertyOptional({
|
|
type: IssueTemplateBodyDto,
|
|
nullable: true,
|
|
description: 'What a new ticket of the type starts with',
|
|
})
|
|
@IsOptional()
|
|
@ValidateNested()
|
|
@Type(() => IssueTemplateBodyDto)
|
|
template?: IssueTemplateBodyDto | null;
|
|
}
|
|
|
|
export class AreaOptionBodyDto {
|
|
@ApiProperty({ example: 'DATA' }) @Matches(SETTING_ID) id!: string;
|
|
@ApiProperty({ example: 'Datos', maxLength: 40 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(40)
|
|
name!: string;
|
|
@ApiPropertyOptional({ type: Boolean, default: false })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
archived = false;
|
|
}
|
|
|
|
export class TeamBodyDto {
|
|
@ApiProperty({ example: 'FRONTEND' }) @Matches(SETTING_ID) id!: string;
|
|
@ApiProperty({ example: 'Frontend', maxLength: 40 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(40)
|
|
name!: string;
|
|
@ApiPropertyOptional({
|
|
type: [String],
|
|
description: 'Pireon user ids of project members (others are dropped)',
|
|
})
|
|
@IsOptional()
|
|
@IsArray()
|
|
@ArrayMaxSize(200)
|
|
@IsString({ each: true })
|
|
memberIds: string[] = [];
|
|
@ApiPropertyOptional({ type: Boolean, default: false })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
archived = false;
|
|
}
|
|
|
|
export class CustomFieldDefinitionBodyDto {
|
|
@ApiProperty({ example: 'BROWSER' }) @Matches(SETTING_ID) id!: string;
|
|
@ApiProperty({ example: 'Navegador', maxLength: 40 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(40)
|
|
name!: string;
|
|
@ApiProperty({
|
|
enum: CustomFieldType,
|
|
enumName: 'CustomFieldType',
|
|
description: 'Cannot change once the field exists. DATE: YYYY-MM-DD',
|
|
})
|
|
@IsEnum(CustomFieldType)
|
|
type!: CustomFieldType;
|
|
@ApiPropertyOptional({
|
|
type: [String],
|
|
maxItems: 50,
|
|
example: ['Chrome', 'Safari', 'Firefox'],
|
|
description: 'The choices of a SELECT field',
|
|
})
|
|
@IsOptional()
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@IsString({ each: true })
|
|
@MaxLength(60, { each: true })
|
|
options: string[] = [];
|
|
@ApiPropertyOptional({ type: Boolean, default: false })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
archived = false;
|
|
}
|
|
|
|
export class ProjectSettingsBodyDto {
|
|
@ApiProperty({
|
|
type: [WorkflowStatusBodyDto],
|
|
description:
|
|
'In board order. Values cannot be removed: archive them instead.',
|
|
})
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => WorkflowStatusBodyDto)
|
|
statuses!: WorkflowStatusBodyDto[];
|
|
|
|
@ApiProperty({ type: [StatusTransitionsBodyDto] })
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => StatusTransitionsBodyDto)
|
|
transitions!: StatusTransitionsBodyDto[];
|
|
|
|
@ApiProperty({ type: [IssueTypeOptionBodyDto] })
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => IssueTypeOptionBodyDto)
|
|
issueTypes!: IssueTypeOptionBodyDto[];
|
|
|
|
@ApiProperty({ type: [AreaOptionBodyDto] })
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => AreaOptionBodyDto)
|
|
areas!: AreaOptionBodyDto[];
|
|
|
|
@ApiPropertyOptional({
|
|
type: [CustomFieldDefinitionBodyDto],
|
|
description: 'Absent: unchanged. Fields cannot be removed: archive them.',
|
|
})
|
|
@IsOptional()
|
|
@IsArray()
|
|
@ArrayMaxSize(30)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => CustomFieldDefinitionBodyDto)
|
|
customFields?: CustomFieldDefinitionBodyDto[];
|
|
|
|
@ApiPropertyOptional({
|
|
type: [TeamBodyDto],
|
|
description: 'Absent: unchanged. Teams cannot be removed: archive them.',
|
|
})
|
|
@IsOptional()
|
|
@IsArray()
|
|
@ArrayMaxSize(30)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => TeamBodyDto)
|
|
teams?: TeamBodyDto[];
|
|
|
|
@ApiPropertyOptional({
|
|
description:
|
|
'Definition of done: tickets reach a DONE status only with their checklist ticked (owners and admins may override). Absent: unchanged',
|
|
})
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
checklistBeforeDone?: boolean;
|
|
}
|
|
|
|
export class AnalyticsQueryDto {
|
|
@ApiPropertyOptional({ type: Number, default: 90, minimum: 7, maximum: 365 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(7)
|
|
@Max(365)
|
|
days = 90;
|
|
}
|
|
|
|
export class InsightsQueryDto {
|
|
@ApiPropertyOptional({
|
|
description:
|
|
'ISO instant (e.g. your last visit): the answer also tells what changed after it',
|
|
example: '2026-10-14T08:00:00Z',
|
|
})
|
|
@IsOptional()
|
|
@IsISO8601()
|
|
since?: string;
|
|
}
|
|
|
|
export class ChangeMemberRoleDto {
|
|
@ApiProperty({ enum: ProjectRole, enumName: 'ProjectRole' })
|
|
@IsEnum(ProjectRole)
|
|
role!: ProjectRole;
|
|
}
|
|
|
|
export class BoardQueryDto {
|
|
@ApiPropertyOptional({ format: 'uuid', description: 'Only this release' })
|
|
@IsOptional()
|
|
@IsString()
|
|
releaseId?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Only this person (Pireon user id)' })
|
|
@IsOptional()
|
|
@IsString()
|
|
assigneeId?: string;
|
|
|
|
@ApiPropertyOptional({ example: 'FRONTEND', description: 'An area id' })
|
|
@IsOptional()
|
|
@Matches(SETTING_ID)
|
|
area?: string;
|
|
|
|
@ApiPropertyOptional({ type: [String], description: 'Ticket type ids' })
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@Matches(SETTING_ID, { each: true })
|
|
type?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
enum: IssuePriority,
|
|
enumName: 'IssuePriority',
|
|
isArray: true,
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsEnum(IssuePriority, { each: true })
|
|
priority?: IssuePriority[];
|
|
|
|
@ApiPropertyOptional({ type: [String] })
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsString({ each: true })
|
|
label?: string[];
|
|
|
|
@ApiPropertyOptional({ description: 'Matches key, title or description' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
search?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Only tickets without assignee' })
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
unassigned?: boolean;
|
|
|
|
@ApiPropertyOptional({ format: 'uuid', description: 'Only this sprint' })
|
|
@IsOptional()
|
|
@IsUUID()
|
|
sprintId?: string;
|
|
|
|
@ApiPropertyOptional({ type: Number, default: 50, minimum: 1, maximum: 200 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(200)
|
|
perColumn = 50;
|
|
}
|
|
|
|
export class ActivityQueryDto {
|
|
@ApiPropertyOptional({ format: 'uuid', description: 'Only this ticket' })
|
|
@IsOptional()
|
|
@IsString()
|
|
issueId?: string;
|
|
|
|
@ApiPropertyOptional({ type: Number, default: 50, minimum: 1, maximum: 200 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(200)
|
|
limit = 50;
|
|
|
|
@ApiPropertyOptional({
|
|
format: 'date-time',
|
|
description: 'Cursor: entries older than this date',
|
|
})
|
|
@IsOptional()
|
|
@IsISO8601()
|
|
before?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
format: 'date-time',
|
|
description:
|
|
'Entries at or after this date (with before, a window such as a day)',
|
|
})
|
|
@IsOptional()
|
|
@IsISO8601()
|
|
after?: string;
|
|
}
|
|
|
|
export class PageQueryDto {
|
|
@ApiPropertyOptional({ type: Number, default: 50, minimum: 1, maximum: 200 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(200)
|
|
limit = 50;
|
|
|
|
@ApiPropertyOptional({ type: Number, default: 0, minimum: 0 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(0)
|
|
offset = 0;
|
|
}
|
|
|
|
export class DecisionsQueryDto {
|
|
@ApiPropertyOptional({ description: 'Words in the decision' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
search?: string;
|
|
|
|
@ApiPropertyOptional({ type: Number, default: 20, minimum: 1, maximum: 100 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(100)
|
|
limit = 20;
|
|
}
|
|
|
|
// ---- Issues -----------------------------------------------------------------
|
|
|
|
export class MyIssuesQueryDto extends PageQueryDto {
|
|
@ApiPropertyOptional({
|
|
type: Boolean,
|
|
default: true,
|
|
description:
|
|
'true: tickets assigned to me. false: any ticket of my projects (e.g. ?assignedToMe=false&category=QA)',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
assignedToMe = true;
|
|
|
|
@ApiPropertyOptional({
|
|
type: Boolean,
|
|
description:
|
|
'Only the tickets I follow (with assignedToMe=false, whoever they are assigned to)',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
watching?: boolean;
|
|
|
|
@ApiPropertyOptional({
|
|
type: [String],
|
|
description: 'Status ids. Repeat the parameter or separate with commas',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@Matches(SETTING_ID, { each: true })
|
|
status?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
enum: StatusCategory,
|
|
enumName: 'StatusCategory',
|
|
isArray: true,
|
|
description: 'Whatever each project calls its statuses',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsEnum(StatusCategory, { each: true })
|
|
category?: StatusCategory[];
|
|
|
|
@ApiPropertyOptional({ description: 'Matches key, title or description' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
search?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
type: [String],
|
|
description:
|
|
'Only these of my projects (ids). Repeat or separate with commas',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsUUID('all', { each: true })
|
|
project?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
enum: IssuePriority,
|
|
enumName: 'IssuePriority',
|
|
isArray: true,
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsEnum(IssuePriority, { each: true })
|
|
priority?: IssuePriority[];
|
|
|
|
@ApiPropertyOptional({ description: 'Only types with the bug icon' })
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
bugsOnly?: boolean;
|
|
|
|
@ApiPropertyOptional({ description: 'Only open tickets past their due date' })
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
overdue?: boolean;
|
|
}
|
|
|
|
export class UpcomingReleasesQueryDto {
|
|
@ApiPropertyOptional({ type: Number, default: 10, minimum: 1, maximum: 50 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(50)
|
|
limit = 10;
|
|
}
|
|
|
|
export class IssueSearchQueryDto extends PageQueryDto {
|
|
@ApiPropertyOptional({
|
|
type: [String],
|
|
description: 'Status ids. Repeat the parameter or separate with commas',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@Matches(SETTING_ID, { each: true })
|
|
status?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
enum: StatusCategory,
|
|
enumName: 'StatusCategory',
|
|
isArray: true,
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsEnum(StatusCategory, { each: true })
|
|
category?: StatusCategory[];
|
|
|
|
@ApiPropertyOptional({ type: [String], description: 'Ticket type ids' })
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@Matches(SETTING_ID, { each: true })
|
|
type?: string[];
|
|
|
|
@ApiPropertyOptional({ type: [String], description: 'Area ids' })
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@Matches(SETTING_ID, { each: true })
|
|
area?: string[];
|
|
|
|
@ApiPropertyOptional({ type: [String], description: 'Team ids' })
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@Matches(SETTING_ID, { each: true })
|
|
team?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
enum: IssuePriority,
|
|
enumName: 'IssuePriority',
|
|
isArray: true,
|
|
})
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsEnum(IssuePriority, { each: true })
|
|
priority?: IssuePriority[];
|
|
|
|
@ApiPropertyOptional({ description: 'Pireon user id' })
|
|
@IsOptional()
|
|
@IsString()
|
|
assigneeId?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Only tickets without assignee' })
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
unassigned?: boolean;
|
|
|
|
@ApiPropertyOptional({ format: 'uuid' })
|
|
@IsOptional()
|
|
@IsString()
|
|
releaseId?: string;
|
|
|
|
@ApiPropertyOptional({ type: [String] })
|
|
@IsOptional()
|
|
@Transform(toList)
|
|
@IsString({ each: true })
|
|
label?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Only the tickets hanging from this one (id or key)',
|
|
example: 'WEB-3',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
parent?: string;
|
|
|
|
@ApiPropertyOptional({ format: 'uuid', description: 'Only this sprint' })
|
|
@IsOptional()
|
|
@IsUUID()
|
|
sprintId?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Only tickets in no sprint' })
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
noSprint?: boolean;
|
|
|
|
@ApiPropertyOptional({
|
|
type: Boolean,
|
|
description: 'Only the tickets I follow (none without a token)',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
watching?: boolean;
|
|
|
|
@ApiPropertyOptional({
|
|
format: 'date',
|
|
description: 'Due on or after this day',
|
|
})
|
|
@IsOptional()
|
|
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
|
dueFrom?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
format: 'date',
|
|
description: 'Due on or before this day',
|
|
})
|
|
@IsOptional()
|
|
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
|
dueTo?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Only tickets nobody estimated' })
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
noEstimate?: boolean;
|
|
|
|
@ApiPropertyOptional({ description: 'Only tickets with no area' })
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
noArea?: boolean;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Only tickets not updated since this instant (ISO)',
|
|
})
|
|
@IsOptional()
|
|
@IsISO8601()
|
|
updatedBefore?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
type: [String],
|
|
example: ['BROWSER:Safari'],
|
|
description:
|
|
'Custom values the tickets must hold, as FIELD:value. Repeat the parameter for several',
|
|
})
|
|
@IsOptional()
|
|
@Transform(({ value }) =>
|
|
value === undefined
|
|
? undefined
|
|
: ([] as unknown[]).concat(value).map(String),
|
|
)
|
|
@IsArray()
|
|
@ArrayMaxSize(30)
|
|
@Matches(/^[A-Za-z][A-Za-z0-9_]{0,29}:[\s\S]+$/, { each: true })
|
|
field?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
enum: IssueSort,
|
|
enumName: 'IssueSort',
|
|
description: 'RECENT (default): last updated first. RANK: backlog order.',
|
|
})
|
|
@IsOptional()
|
|
@IsEnum(IssueSort)
|
|
sort?: IssueSort;
|
|
|
|
@ApiPropertyOptional({ description: 'Matches key, title or description' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
search?: string;
|
|
}
|
|
|
|
class IssueFieldsDto {
|
|
@ApiPropertyOptional({ type: String, nullable: true, maxLength: 50000 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(50000)
|
|
description?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
example: 'FRONTEND',
|
|
description: "An area id of the project's settings",
|
|
})
|
|
@IsOptional()
|
|
@Matches(SETTING_ID)
|
|
area?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
example: 'FRONTEND',
|
|
description: "A team id of the project's settings",
|
|
})
|
|
@IsOptional()
|
|
@Matches(SETTING_ID)
|
|
team?: string | null;
|
|
|
|
@ApiPropertyOptional({ enum: IssuePriority, enumName: 'IssuePriority' })
|
|
@IsOptional()
|
|
@IsEnum(IssuePriority)
|
|
priority?: IssuePriority;
|
|
|
|
@ApiPropertyOptional({ type: [String], maxItems: 20 })
|
|
@IsOptional()
|
|
@ArrayMaxSize(20)
|
|
@IsString({ each: true })
|
|
@MaxLength(40, { each: true })
|
|
labels?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
description: 'Figma frame (https) for UI changes',
|
|
})
|
|
@IsOptional()
|
|
@IsUrl({ protocols: ['https'] })
|
|
figmaUrl?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: Number,
|
|
nullable: true,
|
|
minimum: 0,
|
|
maximum: 100,
|
|
})
|
|
@IsOptional()
|
|
@IsInt()
|
|
@Min(0)
|
|
@Max(100)
|
|
storyPoints?: number | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
format: 'date',
|
|
description: 'When work should start (roadmap)',
|
|
})
|
|
@IsOptional()
|
|
@IsISO8601()
|
|
startDate?: string | null;
|
|
|
|
@ApiPropertyOptional({ type: String, nullable: true, format: 'date' })
|
|
@IsOptional()
|
|
@IsISO8601()
|
|
dueDate?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: 'object',
|
|
additionalProperties: { type: 'string', nullable: true },
|
|
example: { BROWSER: 'Safari' },
|
|
description:
|
|
"Custom field id → value. Only the fields to change; null or '' clears one",
|
|
})
|
|
@IsOptional()
|
|
@IsObject()
|
|
customFields?: Record<string, string | number | null>;
|
|
}
|
|
|
|
export class CreateIssueDto extends IssueFieldsDto {
|
|
@ApiProperty({
|
|
example: 'BUG',
|
|
description: "A ticket type id of the project's settings",
|
|
})
|
|
@Matches(SETTING_ID)
|
|
type!: string;
|
|
|
|
@ApiProperty({
|
|
example: 'El botón de pagar no responde en Safari',
|
|
maxLength: 255,
|
|
})
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(255)
|
|
title!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
description: 'Pireon user id',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
assigneeId?: string | null;
|
|
|
|
@ApiPropertyOptional({ type: String, nullable: true, format: 'uuid' })
|
|
@IsOptional()
|
|
@IsString()
|
|
releaseId?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
example: 'WEB-3',
|
|
description:
|
|
'Epic or ticket of the same project the new one breaks down (id or key)',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
parent?: string | null;
|
|
}
|
|
|
|
export class UpdateIssueDto extends IssueFieldsDto {
|
|
@ApiPropertyOptional({ example: 'FEATURE', description: 'A ticket type id' })
|
|
@IsOptional()
|
|
@Matches(SETTING_ID)
|
|
type?: string;
|
|
|
|
@ApiPropertyOptional({ maxLength: 255 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(255)
|
|
title?: string;
|
|
}
|
|
|
|
export class ChangeStatusDto {
|
|
@ApiProperty({
|
|
example: 'IN_REVIEW',
|
|
description:
|
|
"A status id of the project's settings, among the ticket's allowedTransitions",
|
|
})
|
|
@Matches(SETTING_ID)
|
|
status!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
description: 'Required by statuses with requiresReason (e.g. QA_FAILED)',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(5000)
|
|
reason?: string | null;
|
|
}
|
|
|
|
export class AssignIssueDto {
|
|
@ApiProperty({
|
|
type: String,
|
|
nullable: true,
|
|
description: 'Pireon user id of a project member, or null to unassign',
|
|
})
|
|
@ValidateIf((dto: AssignIssueDto) => dto.assigneeId !== null)
|
|
@IsString()
|
|
assigneeId!: string | null;
|
|
}
|
|
|
|
export class SetIssueReleaseDto {
|
|
@ApiProperty({
|
|
type: String,
|
|
nullable: true,
|
|
format: 'uuid',
|
|
description: 'Planned release, or null to take the ticket out',
|
|
})
|
|
@ValidateIf((dto: SetIssueReleaseDto) => dto.releaseId !== null)
|
|
@IsString()
|
|
releaseId!: string | null;
|
|
}
|
|
|
|
export class SavedFilterCriteriaDto {
|
|
@ApiPropertyOptional({ type: [String], description: 'Ticket type ids' })
|
|
@IsOptional()
|
|
@Matches(SETTING_ID, { each: true })
|
|
types?: string[];
|
|
|
|
@ApiPropertyOptional({ type: [String], description: 'Area ids' })
|
|
@IsOptional()
|
|
@Matches(SETTING_ID, { each: true })
|
|
areas?: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
enum: IssuePriority,
|
|
enumName: 'IssuePriority',
|
|
isArray: true,
|
|
})
|
|
@IsOptional()
|
|
@IsEnum(IssuePriority, { each: true })
|
|
priorities?: IssuePriority[];
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
description: 'Pireon user id',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
assigneeId?: string | null;
|
|
|
|
@ApiPropertyOptional() @IsOptional() @IsBoolean() unassigned?: boolean;
|
|
|
|
@ApiPropertyOptional({ type: String, nullable: true, format: 'uuid' })
|
|
@IsOptional()
|
|
@IsString()
|
|
releaseId?: string | null;
|
|
|
|
@ApiPropertyOptional({ type: [String] })
|
|
@IsOptional()
|
|
@ArrayMaxSize(20)
|
|
@IsString({ each: true })
|
|
labels?: string[];
|
|
|
|
@ApiPropertyOptional({ type: String, nullable: true, maxLength: 200 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
search?: string | null;
|
|
}
|
|
|
|
export class MyViewCriteriaDto {
|
|
@ApiPropertyOptional({
|
|
type: [String],
|
|
format: 'uuid',
|
|
description: 'Only these of my projects (empty: all)',
|
|
})
|
|
@IsOptional()
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@IsUUID('all', { each: true })
|
|
projectIds?: string[];
|
|
@ApiPropertyOptional() @IsOptional() @IsBoolean() assignedToMe?: boolean;
|
|
@ApiPropertyOptional({ description: 'Only the tickets I follow' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
watching?: boolean;
|
|
@ApiPropertyOptional({
|
|
enum: StatusCategory,
|
|
enumName: 'StatusCategory',
|
|
isArray: true,
|
|
})
|
|
@IsOptional()
|
|
@IsEnum(StatusCategory, { each: true })
|
|
categories?: StatusCategory[];
|
|
@ApiPropertyOptional({
|
|
enum: IssuePriority,
|
|
enumName: 'IssuePriority',
|
|
isArray: true,
|
|
})
|
|
@IsOptional()
|
|
@IsEnum(IssuePriority, { each: true })
|
|
priorities?: IssuePriority[];
|
|
@ApiPropertyOptional({ description: 'Only types with the bug icon' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
bugsOnly?: boolean;
|
|
@ApiPropertyOptional({ description: 'Only open tickets past due' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
overdue?: boolean;
|
|
@ApiPropertyOptional({ maxLength: 200 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
search?: string;
|
|
}
|
|
|
|
export class SaveMyViewDto {
|
|
@ApiProperty({
|
|
example: 'Bugs críticos de todos mis proyectos',
|
|
maxLength: 60,
|
|
})
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(60)
|
|
name!: string;
|
|
|
|
@ApiProperty({ type: MyViewCriteriaDto })
|
|
@ValidateNested()
|
|
@Type(() => MyViewCriteriaDto)
|
|
criteria!: MyViewCriteriaDto;
|
|
}
|
|
|
|
export class SaveFilterDto {
|
|
@ApiProperty({ example: 'Bugs críticos de frontend', maxLength: 60 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(60)
|
|
name!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
default: false,
|
|
description: 'Visible to the whole project (members, not viewers, share)',
|
|
})
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
shared = false;
|
|
|
|
@ApiProperty({ type: SavedFilterCriteriaDto })
|
|
@ValidateNested()
|
|
@Type(() => SavedFilterCriteriaDto)
|
|
criteria!: SavedFilterCriteriaDto;
|
|
}
|
|
|
|
export class CreateWebhookDto {
|
|
@ApiProperty({ example: 'https://chat.example.org/hooks/taskstream' })
|
|
@IsUrl({ require_tld: false, require_protocol: true })
|
|
url!: string;
|
|
|
|
@ApiProperty({
|
|
type: [String],
|
|
example: ['issue.status_changed', 'release.*'],
|
|
description: 'Event names (GET /api/webhook-events), family.* or *',
|
|
})
|
|
@IsArray()
|
|
@ArrayMaxSize(50)
|
|
@IsString({ each: true })
|
|
events!: string[];
|
|
}
|
|
|
|
export class UpdateWebhookDto extends PartialType(CreateWebhookDto) {
|
|
@ApiPropertyOptional({ description: 'Paused webhooks get nothing' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
active?: boolean;
|
|
}
|
|
|
|
export class ImportQueryDto {
|
|
@ApiPropertyOptional({
|
|
type: Boolean,
|
|
default: false,
|
|
description: 'Only tell what would be created',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
dryRun = false;
|
|
}
|
|
|
|
export class BulkUpdateIssuesDto {
|
|
@ApiProperty({ type: [String], format: 'uuid', maxItems: 100 })
|
|
@IsArray()
|
|
@ArrayMaxSize(100)
|
|
@IsUUID('all', { each: true })
|
|
issueIds!: string[];
|
|
@ApiPropertyOptional({ example: 'IN_PROGRESS' })
|
|
@IsOptional()
|
|
@Matches(SETTING_ID)
|
|
status?: string;
|
|
@ApiPropertyOptional({ description: 'Unassign everything' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
unassign?: boolean;
|
|
@ApiPropertyOptional({ description: 'Pireon user id of a member' })
|
|
@IsOptional()
|
|
@IsString()
|
|
assigneeId?: string;
|
|
@ApiPropertyOptional({ description: 'Back to the backlog' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
noSprint?: boolean;
|
|
@ApiPropertyOptional({ format: 'uuid' })
|
|
@IsOptional()
|
|
@IsUUID()
|
|
sprintId?: string;
|
|
@ApiPropertyOptional({ description: 'Out of their release' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
noRelease?: boolean;
|
|
@ApiPropertyOptional({ format: 'uuid' })
|
|
@IsOptional()
|
|
@IsUUID()
|
|
releaseId?: string;
|
|
@ApiPropertyOptional({ enum: IssuePriority, enumName: 'IssuePriority' })
|
|
@IsOptional()
|
|
@IsEnum(IssuePriority)
|
|
priority?: IssuePriority;
|
|
@ApiPropertyOptional({ type: [String] })
|
|
@IsOptional()
|
|
@ArrayMaxSize(10)
|
|
@IsString({ each: true })
|
|
addLabels?: string[];
|
|
}
|
|
|
|
export class RankIssueDto {
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
example: 'WEB-4',
|
|
description:
|
|
'The ticket that ends up right above (id or key; null: the top)',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
above?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
example: 'WEB-9',
|
|
description:
|
|
'The ticket that ends up right below (id or key; null: the bottom)',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
below?: string | null;
|
|
}
|
|
|
|
export class SetIssueParentDto {
|
|
@ApiProperty({
|
|
type: String,
|
|
nullable: true,
|
|
example: 'WEB-3',
|
|
description:
|
|
'Epic or ticket of the same project (id or key), or null to take it back to the top',
|
|
})
|
|
@ValidateIf((dto: SetIssueParentDto) => dto.parent !== null)
|
|
@IsString()
|
|
parent!: string | null;
|
|
}
|
|
|
|
export class PresenceBodyDto {
|
|
@ApiPropertyOptional({
|
|
default: false,
|
|
description: 'Editing the ticket or writing a comment',
|
|
})
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
editing = false;
|
|
}
|
|
|
|
export class ChecklistItemBodyDto {
|
|
@ApiProperty({ example: 'Actualizar el changelog', maxLength: 300 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(300)
|
|
text!: string;
|
|
}
|
|
|
|
export class UpdateChecklistItemDto {
|
|
@ApiPropertyOptional({ maxLength: 300 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(300)
|
|
text?: string;
|
|
|
|
@ApiPropertyOptional()
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
done?: boolean;
|
|
}
|
|
|
|
export class LinkIssueDto {
|
|
@ApiProperty({ enum: IssueLinkType, enumName: 'IssueLinkType' })
|
|
@IsEnum(IssueLinkType)
|
|
type!: IssueLinkType;
|
|
|
|
@ApiProperty({ example: 'API-3', description: 'Other ticket: id or key' })
|
|
@IsString()
|
|
@MinLength(1)
|
|
targetIssue!: string;
|
|
}
|
|
|
|
export class LinkTypeParamDto {
|
|
@ApiProperty({ enum: IssueLinkType, enumName: 'IssueLinkType' })
|
|
@IsIn(Object.values(IssueLinkType))
|
|
type!: IssueLinkType;
|
|
}
|
|
|
|
/** Multipart body of an upload; the file itself travels as `file`. */
|
|
export class AttachFileDto {
|
|
@ApiProperty({ type: 'string', format: 'binary', description: 'The file' })
|
|
file?: unknown;
|
|
|
|
@ApiPropertyOptional({
|
|
format: 'uuid',
|
|
description: "One of the ticket's comments, to show the file under it",
|
|
})
|
|
@IsOptional()
|
|
@IsUUID()
|
|
commentId?: string;
|
|
}
|
|
|
|
export class CommentBodyDto {
|
|
@ApiProperty({ example: 'Reproducido en Safari 17.', maxLength: 20000 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(20000)
|
|
body!: string;
|
|
}
|
|
|
|
export class AddCommentDto extends CommentBodyDto {
|
|
@ApiPropertyOptional({
|
|
format: 'uuid',
|
|
description:
|
|
'Comment it answers (one level: answering a reply joins its thread)',
|
|
})
|
|
@IsOptional()
|
|
@IsUUID()
|
|
replyTo?: string;
|
|
}
|
|
|
|
export class ReactToCommentDto {
|
|
@ApiProperty({ enum: REACTIONS, example: '👍' })
|
|
@IsIn(REACTIONS)
|
|
emoji!: string;
|
|
|
|
@ApiProperty({ description: 'true adds the reaction, false takes it back' })
|
|
@IsBoolean()
|
|
on!: boolean;
|
|
}
|
|
|
|
// ---- Public proposals -------------------------------------------------------
|
|
|
|
export class ProposalDto {
|
|
@ApiProperty({
|
|
enum: ['BUG', 'IDEA'],
|
|
description: 'A bug report or an idea',
|
|
})
|
|
@IsIn(['BUG', 'IDEA'])
|
|
kind!: 'BUG' | 'IDEA';
|
|
|
|
@ApiProperty({ minLength: 3, maxLength: 150 })
|
|
@IsString()
|
|
@MinLength(3)
|
|
@MaxLength(150)
|
|
title!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
maxLength: 5000,
|
|
description: 'What happens, or what they would like (Markdown)',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(5000)
|
|
description?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
maxLength: 80,
|
|
description:
|
|
'How they sign it; shown in the ticket (no e-mail: it would be public)',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(80)
|
|
author?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Leave empty: a field only bots fill in',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
website?: string;
|
|
}
|
|
|
|
// ---- Releases ---------------------------------------------------------------
|
|
|
|
export class CreateReleaseDto {
|
|
@ApiProperty({ example: 'v1.4.0', maxLength: 60 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(60)
|
|
name!: string;
|
|
|
|
@ApiPropertyOptional({ type: String, nullable: true, maxLength: 5000 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(5000)
|
|
description?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
format: 'date',
|
|
example: '2026-11-14',
|
|
})
|
|
@IsOptional()
|
|
@IsISO8601()
|
|
targetDate?: string | null;
|
|
}
|
|
|
|
export class UpdateReleaseDto extends PartialType(CreateReleaseDto) {}
|
|
|
|
export class ReleaseNotesDto {
|
|
@ApiProperty({
|
|
type: String,
|
|
nullable: true,
|
|
maxLength: 20000,
|
|
description: 'Markdown; null removes the notes',
|
|
})
|
|
@ValidateIf((dto: ReleaseNotesDto) => dto.notes !== null)
|
|
@IsString()
|
|
@MaxLength(20000)
|
|
notes!: string | null;
|
|
}
|
|
|
|
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
|
export class CreateSprintDto {
|
|
@ApiProperty({ example: 'Sprint 14', maxLength: 80 })
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(80)
|
|
name!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
maxLength: 500,
|
|
example: 'Pagos con tarjeta en producción',
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(500)
|
|
goal?: string | null;
|
|
|
|
@ApiProperty({ format: 'date', example: '2026-10-12' })
|
|
@Matches(DAY)
|
|
startDate!: string;
|
|
|
|
@ApiProperty({
|
|
format: 'date',
|
|
example: '2026-10-23',
|
|
description: 'Last day, included',
|
|
})
|
|
@Matches(DAY)
|
|
endDate!: string;
|
|
}
|
|
|
|
export class UpdateSprintDto extends PartialType(CreateSprintDto) {}
|
|
|
|
export class CloseSprintDto {
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
format: 'uuid',
|
|
description:
|
|
'Open sprint of the same project that takes the unfinished tickets; null or absent: back to the backlog',
|
|
})
|
|
@IsOptional()
|
|
@IsUUID()
|
|
carryOverTo?: string | null;
|
|
}
|
|
|
|
export class SetIssueSprintDto {
|
|
@ApiProperty({
|
|
type: String,
|
|
nullable: true,
|
|
format: 'uuid',
|
|
description: 'Open sprint of the project, or null for the backlog',
|
|
})
|
|
@ValidateIf((dto: SetIssueSprintDto) => dto.sprintId !== null)
|
|
@IsUUID()
|
|
sprintId!: string | null;
|
|
}
|
|
|
|
export class ArchiveReleaseDto {
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
format: 'uuid',
|
|
description:
|
|
'Open release of the same project that takes its unfinished tickets; null or absent: they leave it',
|
|
})
|
|
@IsOptional()
|
|
@IsUUID()
|
|
moveTo?: string | null;
|
|
}
|
|
|
|
export class PublishReleaseDto {
|
|
@ApiPropertyOptional({
|
|
type: Boolean,
|
|
default: false,
|
|
description: 'Publish even with unfinished tickets',
|
|
})
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
force = false;
|
|
}
|
|
|
|
// ---- Notifications ----------------------------------------------------------
|
|
|
|
export class MyNotificationsQueryDto {
|
|
@ApiPropertyOptional({
|
|
type: Boolean,
|
|
default: false,
|
|
description: 'Only unread ones',
|
|
})
|
|
@IsOptional()
|
|
@Transform(toBoolean)
|
|
@IsBoolean()
|
|
unreadOnly = false;
|
|
|
|
@ApiPropertyOptional({ type: Number, default: 30, minimum: 1, maximum: 100 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(100)
|
|
limit = 30;
|
|
}
|
|
|
|
export class MarkNotificationsReadDto {
|
|
@ApiPropertyOptional({
|
|
type: [String],
|
|
description: 'Omit to mark all of them',
|
|
})
|
|
@IsOptional()
|
|
@IsArray()
|
|
@ArrayMaxSize(200)
|
|
@IsString({ each: true })
|
|
ids?: string[];
|
|
}
|
|
|
|
export class NotificationSettingsDto {
|
|
@ApiProperty({ description: 'Copies by e-mail' })
|
|
@IsBoolean()
|
|
email!: boolean;
|
|
}
|
|
|
|
export class PushSubscriptionDto {
|
|
@ApiProperty({
|
|
description: 'The push service URL (PushSubscription.endpoint)',
|
|
})
|
|
@IsUrl({ protocols: ['https'], require_tld: false })
|
|
endpoint!: string;
|
|
|
|
@ApiProperty({ description: 'keys.p256dh of the subscription' })
|
|
@IsString()
|
|
@MaxLength(200)
|
|
p256dh!: string;
|
|
|
|
@ApiProperty({ description: 'keys.auth of the subscription' })
|
|
@IsString()
|
|
@MaxLength(100)
|
|
auth!: string;
|
|
}
|
|
|
|
export class PushEndpointDto {
|
|
@ApiProperty({ description: 'The endpoint of the device to turn off' })
|
|
@IsString()
|
|
@MaxLength(2000)
|
|
endpoint!: string;
|
|
}
|
|
|
|
// ---- Alerts → bugs ----------------------------------------------------------
|
|
|
|
export class AlertSettingsDto {
|
|
@ApiPropertyOptional({
|
|
type: String,
|
|
nullable: true,
|
|
description: "Ticket type to open; null: the project's first bug type",
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
typeId?: string | null;
|
|
|
|
@ApiPropertyOptional({ enum: IssuePriority, enumName: 'IssuePriority' })
|
|
@IsOptional()
|
|
@IsEnum(IssuePriority)
|
|
priority?: IssuePriority;
|
|
}
|
|
|
|
// ---- Git --------------------------------------------------------------------
|
|
|
|
export class AddGitRepositoryDto {
|
|
@ApiProperty({
|
|
example: 'api',
|
|
maxLength: 40,
|
|
description: "The project's name for it (api, web, admin…); unique",
|
|
})
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(40)
|
|
name!: string;
|
|
}
|
|
|
|
export class ShareDashboardDto {
|
|
@ApiProperty({
|
|
type: [String],
|
|
example: ['flow', 'sprint', 'releases'],
|
|
description: 'Blocks to show: flow, sprint, releases, epics, activity',
|
|
})
|
|
@IsArray()
|
|
@ArrayMaxSize(10)
|
|
@IsString({ each: true })
|
|
blocks!: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'A new secret: the old link stops working',
|
|
})
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
renew?: boolean;
|
|
}
|