first commit

This commit is contained in:
2026-10-03 13:40:26 +02:00
parent 732a5b5a16
commit 566f415fa7
555 changed files with 62704 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
export * from './lib/models/enums';
export * from './lib/models/models';
export * from './lib/models/inputs';
export * from './lib/models/workflow';
export * from './lib/ports/gateways';

View File

@@ -0,0 +1,57 @@
export enum IssueType {
BUG = 'BUG',
FEATURE = 'FEATURE',
UI_CHANGE = 'UI_CHANGE',
TASK = 'TASK',
QA = 'QA',
}
export enum IssueArea {
FRONTEND = 'FRONTEND',
BACKEND = 'BACKEND',
MOBILE = 'MOBILE',
QA = 'QA',
DESIGN = 'DESIGN',
DEVOPS = 'DEVOPS',
}
export enum IssuePriority {
LOWEST = 'LOWEST',
LOW = 'LOW',
MEDIUM = 'MEDIUM',
HIGH = 'HIGH',
CRITICAL = 'CRITICAL',
}
export enum IssueStatus {
BACKLOG = 'BACKLOG',
TODO = 'TODO',
IN_PROGRESS = 'IN_PROGRESS',
IN_REVIEW = 'IN_REVIEW',
READY_FOR_QA = 'READY_FOR_QA',
IN_QA = 'IN_QA',
QA_FAILED = 'QA_FAILED',
DONE = 'DONE',
CANCELLED = 'CANCELLED',
}
export enum IssueLinkType {
BLOCKS = 'BLOCKS',
DEPENDS_ON = 'DEPENDS_ON',
TESTS = 'TESTS',
DUPLICATES = 'DUPLICATES',
RELATES_TO = 'RELATES_TO',
}
export enum ProjectRole {
OWNER = 'OWNER',
ADMIN = 'ADMIN',
MEMBER = 'MEMBER',
VIEWER = 'VIEWER',
}
export enum ReleaseStatus {
PLANNED = 'PLANNED',
RELEASED = 'RELEASED',
ARCHIVED = 'ARCHIVED',
}

View File

@@ -0,0 +1,65 @@
import {
IssueArea,
IssueLinkType,
IssuePriority,
IssueStatus,
IssueType,
ProjectRole,
} from './enums';
export interface CreateProjectInput {
key: string;
name: string;
description?: string | null;
}
export interface CreateIssueInput {
projectId: string;
type: IssueType;
title: string;
description?: string | null;
area?: IssueArea | null;
priority?: IssuePriority;
labels?: string[];
figmaUrl?: string | null;
assigneeId?: string | null;
releaseId?: string | null;
}
export type UpdateIssueInput = Partial<
Omit<CreateIssueInput, 'projectId' | 'assigneeId' | 'releaseId'>
> & {
issueId: string;
};
export interface ChangeStatusInput {
issueId: string;
status: IssueStatus;
reason?: string | null;
}
export interface LinkIssuesInput {
issueId: string;
type: IssueLinkType;
targetIssueId: string;
}
export interface MemberInput {
projectId: string;
userId: string;
role: ProjectRole;
}
export interface CreateReleaseInput {
projectId: string;
name: string;
description?: string | null;
targetDate?: string | null;
}
export interface BoardFilter {
projectId: string;
assigneeId?: string;
releaseId?: string;
area?: IssueArea;
}

View File

@@ -0,0 +1,131 @@
import {
IssueArea,
IssueLinkType,
IssuePriority,
IssueStatus,
IssueType,
ProjectRole,
ReleaseStatus,
} from './enums';
export interface UserRef {
id: string;
name: string;
}
export interface ProjectMember {
user: UserRef;
role: ProjectRole;
}
export interface Project {
id: string;
key: string;
name: string;
description: string | null;
owner: UserRef;
members: ProjectMember[];
updatedAt: string;
version: number;
}
export interface IssueLink {
type: IssueLinkType;
/** OUTWARD: this issue → other. INWARD: other → this issue. */
direction: 'OUTWARD' | 'INWARD';
issueId: string;
issueKey: string;
}
/** What boards and lists show. */
export interface IssueSummary {
id: string;
projectId: string;
key: string;
type: IssueType;
area: IssueArea | null;
title: string;
status: IssueStatus;
priority: IssuePriority;
assignee: UserRef | null;
release: { id: string; name: string } | null;
labels: string[];
commentCount: number;
updatedAt: string;
version: number;
}
export interface Issue extends IssueSummary {
description: string | null;
reporter: UserRef;
figmaUrl: string | null;
storyPoints: number | null;
dueDate: string | null;
links: IssueLink[];
allowedTransitions: IssueStatus[];
createdAt: string;
}
export interface Comment {
id: string;
issueId: string;
author: UserRef;
body: string;
createdAt: string;
editedAt: string | null;
}
export interface Release {
id: string;
projectId: string;
name: string;
description: string | null;
targetDate: string | null;
status: ReleaseStatus;
releasedAt: string | null;
progress: { total: number; finished: number; percent: number };
}
export interface Activity {
id: string;
type: string;
issueId: string | null;
issueKey: string | null;
actor: UserRef;
summary: string;
occurredAt: string;
}
export interface CountBy<T = string> {
key: T;
count: number;
}
export interface ProjectStats {
total: number;
open: number;
byStatus: CountBy<IssueStatus>[];
byType: CountBy[];
byPriority: CountBy[];
byArea: CountBy[];
byAssignee: { user: UserRef | null; count: number }[];
}
export interface BoardColumn {
status: IssueStatus;
issues: IssueSummary[];
total: number;
}
export interface UserDirectoryEntry {
id: string;
name: string;
email: string;
}
/** Ack of a command: the read model catches up asynchronously. */
export interface CommandResult {
id: string;
key?: string | null;
version: number;
}

View File

@@ -0,0 +1,50 @@
import { IssuePriority, IssueStatus, IssueType } from './enums';
/** Statuses that require an explanation (the API rejects them without one). */
export const REASON_REQUIRED: readonly IssueStatus[] = [
IssueStatus.QA_FAILED,
IssueStatus.CANCELLED,
];
export const BOARD_COLUMNS: readonly IssueStatus[] = [
IssueStatus.BACKLOG,
IssueStatus.TODO,
IssueStatus.IN_PROGRESS,
IssueStatus.IN_REVIEW,
IssueStatus.READY_FOR_QA,
IssueStatus.IN_QA,
IssueStatus.QA_FAILED,
IssueStatus.DONE,
];
export const STATUS_LABEL: Record<IssueStatus, string> = {
[IssueStatus.BACKLOG]: 'Backlog',
[IssueStatus.TODO]: 'To do',
[IssueStatus.IN_PROGRESS]: 'In progress',
[IssueStatus.IN_REVIEW]: 'In review',
[IssueStatus.READY_FOR_QA]: 'Ready for QA',
[IssueStatus.IN_QA]: 'In QA',
[IssueStatus.QA_FAILED]: 'QA failed',
[IssueStatus.DONE]: 'Done',
[IssueStatus.CANCELLED]: 'Cancelled',
};
export const TYPE_LABEL: Record<IssueType, string> = {
[IssueType.BUG]: 'Bug',
[IssueType.FEATURE]: 'Feature',
[IssueType.UI_CHANGE]: 'UI change',
[IssueType.TASK]: 'Task',
[IssueType.QA]: 'QA',
};
export const PRIORITY_ORDER: readonly IssuePriority[] = [
IssuePriority.CRITICAL,
IssuePriority.HIGH,
IssuePriority.MEDIUM,
IssuePriority.LOW,
IssuePriority.LOWEST,
];
export function isFinished(status: IssueStatus): boolean {
return status === IssueStatus.DONE || status === IssueStatus.CANCELLED;
}

View File

@@ -0,0 +1,108 @@
import { Observable } from 'rxjs';
import {
Activity,
BoardColumn,
Comment,
CommandResult,
Issue,
IssueSummary,
Project,
ProjectStats,
Release,
UserDirectoryEntry,
} from '../models/models';
import {
BoardFilter,
ChangeStatusInput,
CreateIssueInput,
CreateProjectInput,
CreateReleaseInput,
LinkIssuesInput,
MemberInput,
UpdateIssueInput,
} from '../models/inputs';
/*
* Outbound ports of the tracker. The application layer depends on these
* abstract classes; the infrastructure layer implements them over GraphQL.
*/
export abstract class ProjectsGateway {
abstract myProjects(): Observable<Project[]>;
abstract project(idOrKey: string): Observable<Project>;
abstract stats(projectId: string): Observable<ProjectStats>;
abstract create(input: CreateProjectInput): Observable<CommandResult>;
abstract addMember(input: MemberInput): Observable<CommandResult>;
abstract changeMemberRole(input: MemberInput): Observable<CommandResult>;
abstract removeMember(
projectId: string,
userId: string,
): Observable<CommandResult>;
}
export abstract class IssuesGateway {
abstract board(filter: BoardFilter): Observable<BoardColumn[]>;
abstract issue(idOrKey: string): Observable<Issue>;
abstract search(projectId: string, text: string): Observable<IssueSummary[]>;
abstract create(input: CreateIssueInput): Observable<CommandResult>;
abstract update(input: UpdateIssueInput): Observable<CommandResult>;
abstract changeStatus(input: ChangeStatusInput): Observable<CommandResult>;
abstract assign(
issueId: string,
assigneeId: string | null,
): Observable<CommandResult>;
abstract link(input: LinkIssuesInput): Observable<CommandResult>;
abstract unlink(input: LinkIssuesInput): Observable<CommandResult>;
abstract setRelease(
issueId: string,
releaseId: string | null,
): Observable<CommandResult>;
}
export abstract class CommentsGateway {
abstract list(issueId: string): Observable<Comment[]>;
abstract add(issueId: string, body: string): Observable<CommandResult>;
abstract edit(commentId: string, body: string): Observable<CommandResult>;
abstract remove(commentId: string): Observable<CommandResult>;
}
export abstract class ReleasesGateway {
abstract list(projectId: string): Observable<Release[]>;
abstract create(input: CreateReleaseInput): Observable<CommandResult>;
abstract publish(
releaseId: string,
force: boolean,
): Observable<CommandResult>;
abstract archive(releaseId: string): Observable<CommandResult>;
}
export abstract class ActivityGateway {
abstract list(projectId: string, issueId?: string): Observable<Activity[]>;
}
export abstract class UsersGateway {
abstract search(term: string): Observable<UserDirectoryEntry[]>;
}
/** Realtime (GraphQL subscriptions over WebSocket). */
export interface IssueChanged {
action: string;
version: number;
actorId: string;
issue: IssueSummary;
}
export interface CommentChanged {
action: string;
commentId: string;
comment: Comment | null;
}
export abstract class TrackerRealtimeGateway {
abstract issueChanges(projectId: string): Observable<IssueChanged>;
abstract commentChanges(issueId: string): Observable<CommentChanged>;
abstract activity(projectId: string): Observable<Activity>;
abstract releaseChanges(projectId: string): Observable<Release>;
abstract projectChanges(projectId: string): Observable<Project>;
abstract myProjectsChanges(): Observable<Project>;
}