first commit
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
Activity,
|
||||
ActivityGateway,
|
||||
CommandResult,
|
||||
Comment,
|
||||
CommentsGateway,
|
||||
CreateReleaseInput,
|
||||
Release,
|
||||
ReleasesGateway,
|
||||
UserDirectoryEntry,
|
||||
UsersGateway,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import {
|
||||
ActivityDocument,
|
||||
AddCommentDocument,
|
||||
ArchiveReleaseDocument,
|
||||
CreateReleaseDocument,
|
||||
DeleteCommentDocument,
|
||||
EditCommentDocument,
|
||||
IssueCommentsDocument,
|
||||
PublishReleaseDocument,
|
||||
ReleasesDocument,
|
||||
SearchUsersDocument,
|
||||
} from '../graphql/generated';
|
||||
import { GraphqlOperations } from './graphql-operations';
|
||||
import { toActivity, toCommandResult, toComment, toRelease } from './mappers';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlCommentsGateway extends CommentsGateway {
|
||||
private readonly gql = inject(GraphqlOperations);
|
||||
|
||||
list(issueId: string): Observable<Comment[]> {
|
||||
return this.gql
|
||||
.query(IssueCommentsDocument, { idOrKey: issueId })
|
||||
.pipe(map((d) => d.issue.comments.items.map(toComment)));
|
||||
}
|
||||
|
||||
add(issueId: string, body: string): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(AddCommentDocument, { issueId, body })
|
||||
.pipe(map((d) => toCommandResult(d.addComment)));
|
||||
}
|
||||
|
||||
edit(commentId: string, body: string): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(EditCommentDocument, { commentId, body })
|
||||
.pipe(map((d) => toCommandResult(d.editComment)));
|
||||
}
|
||||
|
||||
remove(commentId: string): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(DeleteCommentDocument, { commentId })
|
||||
.pipe(map((d) => toCommandResult(d.deleteComment)));
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlReleasesGateway extends ReleasesGateway {
|
||||
private readonly gql = inject(GraphqlOperations);
|
||||
|
||||
list(projectId: string): Observable<Release[]> {
|
||||
return this.gql
|
||||
.query(ReleasesDocument, { idOrKey: projectId })
|
||||
.pipe(map((d) => d.project.releases.map(toRelease)));
|
||||
}
|
||||
|
||||
create(input: CreateReleaseInput): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(CreateReleaseDocument, { input })
|
||||
.pipe(map((d) => toCommandResult(d.createRelease)));
|
||||
}
|
||||
|
||||
publish(releaseId: string, force: boolean): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(PublishReleaseDocument, { releaseId, force })
|
||||
.pipe(map((d) => toCommandResult(d.publishRelease)));
|
||||
}
|
||||
|
||||
archive(releaseId: string): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(ArchiveReleaseDocument, { releaseId })
|
||||
.pipe(map((d) => toCommandResult(d.archiveRelease)));
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlActivityGateway extends ActivityGateway {
|
||||
private readonly gql = inject(GraphqlOperations);
|
||||
|
||||
list(projectId: string, issueId?: string): Observable<Activity[]> {
|
||||
return this.gql
|
||||
.query(ActivityDocument, { projectId, issueId: issueId ?? null })
|
||||
.pipe(map((d) => d.activity.map(toActivity)));
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlUsersGateway extends UsersGateway {
|
||||
private readonly gql = inject(GraphqlOperations);
|
||||
|
||||
search(term: string): Observable<UserDirectoryEntry[]> {
|
||||
return this.gql
|
||||
.query(SearchUsersDocument, { search: term })
|
||||
.pipe(map((d) => d.users));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
BoardColumn,
|
||||
BoardFilter,
|
||||
ChangeStatusInput,
|
||||
CommandResult,
|
||||
CreateIssueInput,
|
||||
Issue,
|
||||
IssuesGateway,
|
||||
IssueSummary,
|
||||
LinkIssuesInput,
|
||||
UpdateIssueInput,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import {
|
||||
AssignIssueDocument,
|
||||
BoardDocument,
|
||||
ChangeIssueStatusDocument,
|
||||
CreateIssueDocument,
|
||||
IssueDocument,
|
||||
LinkIssuesDocument,
|
||||
SearchIssuesDocument,
|
||||
SetIssueReleaseDocument,
|
||||
UnlinkIssuesDocument,
|
||||
UpdateIssueDocument,
|
||||
} from '../graphql/generated';
|
||||
import { GraphqlOperations } from './graphql-operations';
|
||||
import { toCommandResult, toIssue, toIssueSummary } from './mappers';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlIssuesGateway extends IssuesGateway {
|
||||
private readonly gql = inject(GraphqlOperations);
|
||||
|
||||
board(filter: BoardFilter): Observable<BoardColumn[]> {
|
||||
return this.gql
|
||||
.query(BoardDocument, {
|
||||
idOrKey: filter.projectId,
|
||||
releaseId: filter.releaseId ?? null,
|
||||
assigneeId: filter.assigneeId ?? null,
|
||||
area: filter.area ?? null,
|
||||
})
|
||||
.pipe(
|
||||
map((d) =>
|
||||
d.project.board.map((column) => ({
|
||||
status: column.status as BoardColumn['status'],
|
||||
total: column.total,
|
||||
issues: column.issues.map(toIssueSummary),
|
||||
})),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
issue(idOrKey: string): Observable<Issue> {
|
||||
return this.gql
|
||||
.query(IssueDocument, { idOrKey })
|
||||
.pipe(map((d) => toIssue(d.issue)));
|
||||
}
|
||||
|
||||
search(projectId: string, text: string): Observable<IssueSummary[]> {
|
||||
return this.gql
|
||||
.query(SearchIssuesDocument, { filter: { projectId, search: text } })
|
||||
.pipe(map((d) => d.issues.items.map(toIssueSummary)));
|
||||
}
|
||||
|
||||
create(input: CreateIssueInput): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(CreateIssueDocument, { input: input as never })
|
||||
.pipe(map((d) => toCommandResult(d.createIssue)));
|
||||
}
|
||||
|
||||
update(input: UpdateIssueInput): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(UpdateIssueDocument, { input: input as never })
|
||||
.pipe(map((d) => toCommandResult(d.updateIssue)));
|
||||
}
|
||||
|
||||
changeStatus(input: ChangeStatusInput): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(ChangeIssueStatusDocument, { input: input as never })
|
||||
.pipe(map((d) => toCommandResult(d.changeIssueStatus)));
|
||||
}
|
||||
|
||||
assign(
|
||||
issueId: string,
|
||||
assigneeId: string | null,
|
||||
): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(AssignIssueDocument, { issueId, assigneeId })
|
||||
.pipe(map((d) => toCommandResult(d.assignIssue)));
|
||||
}
|
||||
|
||||
link(input: LinkIssuesInput): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(LinkIssuesDocument, { input: input as never })
|
||||
.pipe(map((d) => toCommandResult(d.linkIssues)));
|
||||
}
|
||||
|
||||
unlink(input: LinkIssuesInput): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(UnlinkIssuesDocument, { input: input as never })
|
||||
.pipe(map((d) => toCommandResult(d.unlinkIssues)));
|
||||
}
|
||||
|
||||
setRelease(
|
||||
issueId: string,
|
||||
releaseId: string | null,
|
||||
): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(SetIssueReleaseDocument, { issueId, releaseId })
|
||||
.pipe(map((d) => toCommandResult(d.setIssueRelease)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
||||
import { Apollo } from 'apollo-angular';
|
||||
import { filter, map, Observable } from 'rxjs';
|
||||
|
||||
type Vars = Record<string, unknown>;
|
||||
|
||||
/** Thin typed wrapper: one-shot queries/mutations and subscriptions as plain data streams. */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class GraphqlOperations {
|
||||
private readonly apollo = inject(Apollo);
|
||||
|
||||
query<TData, TVars extends Vars>(
|
||||
document: TypedDocumentNode<TData, TVars>,
|
||||
variables?: TVars,
|
||||
): Observable<TData> {
|
||||
return this.apollo
|
||||
.query<TData, TVars>({ query: document, variables: variables as TVars })
|
||||
.pipe(map((result) => result.data as TData));
|
||||
}
|
||||
|
||||
mutate<TData, TVars extends Vars>(
|
||||
document: TypedDocumentNode<TData, TVars>,
|
||||
variables: TVars,
|
||||
): Observable<TData> {
|
||||
return this.apollo
|
||||
.mutate<TData, TVars>({ mutation: document, variables })
|
||||
.pipe(map((result) => result.data as TData));
|
||||
}
|
||||
|
||||
subscribe<TData, TVars extends Vars>(
|
||||
document: TypedDocumentNode<TData, TVars>,
|
||||
variables?: TVars,
|
||||
): Observable<TData> {
|
||||
return this.apollo
|
||||
.subscribe<
|
||||
TData,
|
||||
TVars
|
||||
>({ query: document, variables: variables as TVars })
|
||||
.pipe(
|
||||
map((result) => result.data),
|
||||
filter((data): data is NonNullable<typeof data> => !!data),
|
||||
map((data) => data as TData),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
CommandResult,
|
||||
CreateProjectInput,
|
||||
MemberInput,
|
||||
Project,
|
||||
ProjectsGateway,
|
||||
ProjectStats,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import {
|
||||
AddProjectMemberDocument,
|
||||
ChangeProjectMemberRoleDocument,
|
||||
CreateProjectDocument,
|
||||
MyProjectsDocument,
|
||||
ProjectDocument,
|
||||
ProjectStatsDocument,
|
||||
RemoveProjectMemberDocument,
|
||||
} from '../graphql/generated';
|
||||
import { GraphqlOperations } from './graphql-operations';
|
||||
import { toCommandResult, toProject, toStats } from './mappers';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlProjectsGateway extends ProjectsGateway {
|
||||
private readonly gql = inject(GraphqlOperations);
|
||||
|
||||
myProjects(): Observable<Project[]> {
|
||||
return this.gql
|
||||
.query(MyProjectsDocument)
|
||||
.pipe(map((d) => d.myProjects.map(toProject)));
|
||||
}
|
||||
|
||||
project(idOrKey: string): Observable<Project> {
|
||||
return this.gql
|
||||
.query(ProjectDocument, { idOrKey })
|
||||
.pipe(map((d) => toProject(d.project)));
|
||||
}
|
||||
|
||||
stats(projectId: string): Observable<ProjectStats> {
|
||||
return this.gql
|
||||
.query(ProjectStatsDocument, { idOrKey: projectId })
|
||||
.pipe(map((d) => toStats(d.project.stats)));
|
||||
}
|
||||
|
||||
create(input: CreateProjectInput): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(CreateProjectDocument, { input })
|
||||
.pipe(map((d) => toCommandResult(d.createProject)));
|
||||
}
|
||||
|
||||
addMember(input: MemberInput): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(AddProjectMemberDocument, { input: input as never })
|
||||
.pipe(map((d) => toCommandResult(d.addProjectMember)));
|
||||
}
|
||||
|
||||
changeMemberRole(input: MemberInput): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(ChangeProjectMemberRoleDocument, { input: input as never })
|
||||
.pipe(map((d) => toCommandResult(d.changeProjectMemberRole)));
|
||||
}
|
||||
|
||||
removeMember(projectId: string, userId: string): Observable<CommandResult> {
|
||||
return this.gql
|
||||
.mutate(RemoveProjectMemberDocument, { projectId, userId })
|
||||
.pipe(map((d) => toCommandResult(d.removeProjectMember)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
Activity,
|
||||
CommentChanged,
|
||||
IssueChanged,
|
||||
Project,
|
||||
Release,
|
||||
TrackerRealtimeGateway,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import {
|
||||
ActivityAddedDocument,
|
||||
CommentChangedDocument,
|
||||
IssueChangedDocument,
|
||||
MyProjectsChangedDocument,
|
||||
ProjectChangedDocument,
|
||||
ReleaseChangedDocument,
|
||||
} from '../graphql/generated';
|
||||
import { GraphqlOperations } from './graphql-operations';
|
||||
import {
|
||||
toActivity,
|
||||
toComment,
|
||||
toIssueSummary,
|
||||
toProject,
|
||||
toRelease,
|
||||
} from './mappers';
|
||||
|
||||
/** GraphQL subscriptions (graphql-ws). Each stream lives while it is subscribed. */
|
||||
@Injectable()
|
||||
export class GraphqlRealtimeGateway extends TrackerRealtimeGateway {
|
||||
private readonly gql = inject(GraphqlOperations);
|
||||
|
||||
issueChanges(projectId: string): Observable<IssueChanged> {
|
||||
return this.gql.subscribe(IssueChangedDocument, { projectId }).pipe(
|
||||
map(({ issueChanged }) => ({
|
||||
...issueChanged,
|
||||
issue: toIssueSummary(issueChanged.issue),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
commentChanges(issueId: string): Observable<CommentChanged> {
|
||||
return this.gql.subscribe(CommentChangedDocument, { issueId }).pipe(
|
||||
map(({ commentChanged }) => ({
|
||||
action: commentChanged.action,
|
||||
commentId: commentChanged.commentId,
|
||||
comment: commentChanged.comment
|
||||
? toComment(commentChanged.comment)
|
||||
: null,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
activity(projectId: string): Observable<Activity> {
|
||||
return this.gql
|
||||
.subscribe(ActivityAddedDocument, { projectId })
|
||||
.pipe(map((d) => toActivity(d.activityAdded)));
|
||||
}
|
||||
|
||||
releaseChanges(projectId: string): Observable<Release> {
|
||||
return this.gql
|
||||
.subscribe(ReleaseChangedDocument, { projectId })
|
||||
.pipe(map((d) => toRelease(d.releaseChanged.release)));
|
||||
}
|
||||
|
||||
projectChanges(projectId: string): Observable<Project> {
|
||||
return this.gql
|
||||
.subscribe(ProjectChangedDocument, { projectId })
|
||||
.pipe(map((d) => toProject(d.projectChanged.project)));
|
||||
}
|
||||
|
||||
myProjectsChanges(): Observable<Project> {
|
||||
return this.gql
|
||||
.subscribe(MyProjectsChangedDocument)
|
||||
.pipe(map((d) => toProject(d.myProjectsChanged.project)));
|
||||
}
|
||||
}
|
||||
55
libs/web/tracker/infrastructure/src/lib/adapters/mappers.ts
Normal file
55
libs/web/tracker/infrastructure/src/lib/adapters/mappers.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type {
|
||||
Activity,
|
||||
Comment,
|
||||
CommandResult,
|
||||
Issue,
|
||||
IssueSummary,
|
||||
Project,
|
||||
ProjectStats,
|
||||
Release,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
import type {
|
||||
ActivityFieldsFragment,
|
||||
CommandResultFieldsFragment,
|
||||
CommentFieldsFragment,
|
||||
IssueFieldsFragment,
|
||||
IssueSummaryFieldsFragment,
|
||||
ProjectFieldsFragment,
|
||||
ProjectStatsQuery,
|
||||
ReleaseFieldsFragment,
|
||||
} from '../graphql/generated';
|
||||
|
||||
/*
|
||||
* GraphQL → domain. The generated types already have the domain's shape;
|
||||
* enums arrive as string unions with the same values as the domain enums,
|
||||
* so mapping is a typed re-labelling kept in one place.
|
||||
*/
|
||||
|
||||
const as = <T>(value: unknown): T => value as T;
|
||||
|
||||
export const toProject = (p: ProjectFieldsFragment): Project => as<Project>(p);
|
||||
export const toIssueSummary = (i: IssueSummaryFieldsFragment): IssueSummary =>
|
||||
as<IssueSummary>(i);
|
||||
export const toIssue = (i: IssueFieldsFragment): Issue => as<Issue>(i);
|
||||
export const toComment = (c: CommentFieldsFragment): Comment => c;
|
||||
export const toActivity = (a: ActivityFieldsFragment): Activity => a;
|
||||
export const toCommandResult = (
|
||||
r: CommandResultFieldsFragment,
|
||||
): CommandResult => r;
|
||||
|
||||
export function toRelease(
|
||||
r: ReleaseFieldsFragment & {
|
||||
progress?: { total: number; finished: number; percent: number };
|
||||
},
|
||||
): Release {
|
||||
return as<Release>({
|
||||
...r,
|
||||
progress: r.progress ?? { total: 0, finished: 0, percent: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
export function toStats(
|
||||
stats: NonNullable<ProjectStatsQuery['project']>['stats'],
|
||||
): ProjectStats {
|
||||
return as<ProjectStats>(stats);
|
||||
}
|
||||
Reference in New Issue
Block a user