first commit
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { ReadAccess } from './read-access';
|
||||
import { CanViewProjectQuery } from './can-view-project.query';
|
||||
|
||||
@QueryHandler(CanViewProjectQuery)
|
||||
export class CanViewProjectQueryHandler
|
||||
implements IQueryHandler<CanViewProjectQuery, boolean>
|
||||
{
|
||||
constructor(private readonly access: ReadAccess) {}
|
||||
|
||||
async execute(query: CanViewProjectQuery): Promise<boolean> {
|
||||
return this.access.canView(query.projectId, query.viewerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
|
||||
export class CanViewProjectQuery extends Query<boolean> {
|
||||
constructor(
|
||||
public readonly viewerId: string,
|
||||
public readonly projectId: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { ActivityReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { FindActivityRepository } from '../ports/find-activity.repository';
|
||||
import { ReadAccess } from './read-access';
|
||||
import { GetActivityQuery } from './get-activity.query';
|
||||
|
||||
@QueryHandler(GetActivityQuery)
|
||||
export class GetActivityQueryHandler
|
||||
implements IQueryHandler<GetActivityQuery, ActivityReadModel[]>
|
||||
{
|
||||
constructor(
|
||||
private readonly findActivity: FindActivityRepository,
|
||||
private readonly access: ReadAccess,
|
||||
) {}
|
||||
|
||||
async execute(query: GetActivityQuery): Promise<ActivityReadModel[]> {
|
||||
await this.access.assertCanView(query.projectId, query.viewerId);
|
||||
return this.findActivity.find({
|
||||
projectId: query.projectId,
|
||||
issueId: query.issueId,
|
||||
limit: Math.min(query.limit, 200),
|
||||
before: query.before,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { ActivityReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export class GetActivityQuery extends Query<ActivityReadModel[]> {
|
||||
constructor(
|
||||
public readonly viewerId: string,
|
||||
public readonly projectId: string,
|
||||
public readonly issueId?: string,
|
||||
public readonly limit = 50,
|
||||
/** ISO date cursor: only activity strictly older than this. */
|
||||
public readonly before?: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { IssueStatus } from '@task-stream/api/tracker/domain';
|
||||
import { FindIssuesRepository } from '../ports/find-issues.repository';
|
||||
import { BoardColumn, GetBoardQuery } from './get-board.query';
|
||||
import { ReadAccess } from './read-access';
|
||||
|
||||
const BOARD_COLUMNS: IssueStatus[] = [
|
||||
IssueStatus.BACKLOG,
|
||||
IssueStatus.TODO,
|
||||
IssueStatus.IN_PROGRESS,
|
||||
IssueStatus.IN_REVIEW,
|
||||
IssueStatus.READY_FOR_QA,
|
||||
IssueStatus.IN_QA,
|
||||
IssueStatus.QA_FAILED,
|
||||
IssueStatus.DONE,
|
||||
];
|
||||
|
||||
@QueryHandler(GetBoardQuery)
|
||||
export class GetBoardQueryHandler
|
||||
implements IQueryHandler<GetBoardQuery, BoardColumn[]>
|
||||
{
|
||||
constructor(
|
||||
private readonly findIssues: FindIssuesRepository,
|
||||
private readonly access: ReadAccess,
|
||||
) {}
|
||||
|
||||
async execute(query: GetBoardQuery): Promise<BoardColumn[]> {
|
||||
await this.access.assertCanView(query.filter.projectId, query.viewerId);
|
||||
return Promise.all(
|
||||
BOARD_COLUMNS.map(async (status) => {
|
||||
const page = await this.findIssues.search({
|
||||
...query.filter,
|
||||
statuses: [status],
|
||||
limit: Math.min(query.perColumn, 200),
|
||||
offset: 0,
|
||||
});
|
||||
return { status, issues: page.items, total: page.total };
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { IssueReadModel, IssueStatus } from '@task-stream/api/tracker/domain';
|
||||
import { IssueFilter } from '../ports/find-issues.repository';
|
||||
|
||||
export interface BoardColumn {
|
||||
status: IssueStatus;
|
||||
issues: IssueReadModel[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** Kanban board: one column per status. */
|
||||
export class GetBoardQuery extends Query<BoardColumn[]> {
|
||||
constructor(
|
||||
public readonly viewerId: string,
|
||||
public readonly filter: Omit<IssueFilter, 'statuses' | 'limit' | 'offset'>,
|
||||
public readonly perColumn = 50,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { CommentReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { FindCommentsRepository } from '../ports/find-comments.repository';
|
||||
import { FindIssuesRepository, Page } from '../ports/find-issues.repository';
|
||||
import { ReadAccess } from './read-access';
|
||||
import { GetCommentsQuery } from './get-comments.query';
|
||||
|
||||
@QueryHandler(GetCommentsQuery)
|
||||
export class GetCommentsQueryHandler
|
||||
implements IQueryHandler<GetCommentsQuery, Page<CommentReadModel>>
|
||||
{
|
||||
constructor(
|
||||
private readonly findIssues: FindIssuesRepository,
|
||||
private readonly findComments: FindCommentsRepository,
|
||||
private readonly access: ReadAccess,
|
||||
) {}
|
||||
|
||||
async execute(query: GetCommentsQuery): Promise<Page<CommentReadModel>> {
|
||||
const issue = this.access.assertFound(
|
||||
await this.findIssues.findById(query.issueId),
|
||||
'Issue',
|
||||
query.issueId,
|
||||
);
|
||||
await this.access.assertCanView(issue.projectId, query.viewerId);
|
||||
return this.findComments.findByIssue(
|
||||
issue.id,
|
||||
Math.min(query.limit, 200),
|
||||
query.offset,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { CommentReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { Page } from '../ports/find-issues.repository';
|
||||
|
||||
export class GetCommentsQuery extends Query<Page<CommentReadModel>> {
|
||||
constructor(
|
||||
public readonly viewerId: string,
|
||||
public readonly issueId: string,
|
||||
public readonly limit = 50,
|
||||
public readonly offset = 0,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { IssueReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { FindIssuesRepository } from '../ports/find-issues.repository';
|
||||
import { ReadAccess, UUID } from './read-access';
|
||||
import { GetIssueQuery } from './get-issue.query';
|
||||
|
||||
@QueryHandler(GetIssueQuery)
|
||||
export class GetIssueQueryHandler
|
||||
implements IQueryHandler<GetIssueQuery, IssueReadModel>
|
||||
{
|
||||
constructor(
|
||||
private readonly findIssues: FindIssuesRepository,
|
||||
private readonly access: ReadAccess,
|
||||
) {}
|
||||
|
||||
async execute(query: GetIssueQuery): Promise<IssueReadModel> {
|
||||
const issue = UUID.test(query.idOrKey)
|
||||
? await this.findIssues.findById(query.idOrKey)
|
||||
: await this.findIssues.findByKey(query.idOrKey.toUpperCase());
|
||||
const found = this.access.assertFound(issue, 'Issue', query.idOrKey);
|
||||
await this.access.assertCanView(found.projectId, query.viewerId);
|
||||
return found;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { IssueReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export class GetIssueQuery extends Query<IssueReadModel> {
|
||||
constructor(
|
||||
public readonly viewerId: string,
|
||||
/** Issue id or key ("TS-42"). */
|
||||
public readonly idOrKey: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { IssueReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { FindIssuesRepository } from '../ports/find-issues.repository';
|
||||
import { FindProjectsRepository } from '../ports/find-projects.repository';
|
||||
import { GetIssuesByIdsQuery } from './get-issues-by-ids.query';
|
||||
|
||||
@QueryHandler(GetIssuesByIdsQuery)
|
||||
export class GetIssuesByIdsQueryHandler
|
||||
implements IQueryHandler<GetIssuesByIdsQuery, IssueReadModel[]>
|
||||
{
|
||||
constructor(
|
||||
private readonly findIssues: FindIssuesRepository,
|
||||
private readonly findProjects: FindProjectsRepository,
|
||||
) {}
|
||||
|
||||
async execute(query: GetIssuesByIdsQuery): Promise<IssueReadModel[]> {
|
||||
const issues = await this.findIssues.findByIds([...new Set(query.ids)]);
|
||||
const visible = new Set(
|
||||
(await this.findProjects.findByMember(query.viewerId)).map((p) => p.id),
|
||||
);
|
||||
return issues.filter((issue) => visible.has(issue.projectId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { IssueReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export class GetIssuesByIdsQuery extends Query<IssueReadModel[]> {
|
||||
constructor(
|
||||
public readonly viewerId: string,
|
||||
public readonly ids: string[],
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { ProjectReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { FindProjectsRepository } from '../ports/find-projects.repository';
|
||||
import { GetMyProjectsQuery } from './get-my-projects.query';
|
||||
|
||||
@QueryHandler(GetMyProjectsQuery)
|
||||
export class GetMyProjectsQueryHandler
|
||||
implements IQueryHandler<GetMyProjectsQuery, ProjectReadModel[]>
|
||||
{
|
||||
constructor(private readonly findProjects: FindProjectsRepository) {}
|
||||
|
||||
async execute(query: GetMyProjectsQuery): Promise<ProjectReadModel[]> {
|
||||
return this.findProjects.findByMember(query.viewerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { ProjectReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export class GetMyProjectsQuery extends Query<ProjectReadModel[]> {
|
||||
constructor(public readonly viewerId: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { ProjectStatsReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { FindIssuesRepository } from '../ports/find-issues.repository';
|
||||
import { ReadAccess } from './read-access';
|
||||
import { GetProjectStatsQuery } from './get-project-stats.query';
|
||||
|
||||
@QueryHandler(GetProjectStatsQuery)
|
||||
export class GetProjectStatsQueryHandler
|
||||
implements IQueryHandler<GetProjectStatsQuery, ProjectStatsReadModel>
|
||||
{
|
||||
constructor(
|
||||
private readonly findIssues: FindIssuesRepository,
|
||||
private readonly access: ReadAccess,
|
||||
) {}
|
||||
|
||||
async execute(query: GetProjectStatsQuery): Promise<ProjectStatsReadModel> {
|
||||
await this.access.assertCanView(query.projectId, query.viewerId);
|
||||
return this.findIssues.stats(query.projectId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { ProjectStatsReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export class GetProjectStatsQuery extends Query<ProjectStatsReadModel> {
|
||||
constructor(
|
||||
public readonly viewerId: string,
|
||||
public readonly projectId: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { ProjectReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { FindProjectsRepository } from '../ports/find-projects.repository';
|
||||
import { ReadAccess, UUID } from './read-access';
|
||||
import { GetProjectQuery } from './get-project.query';
|
||||
|
||||
@QueryHandler(GetProjectQuery)
|
||||
export class GetProjectQueryHandler
|
||||
implements IQueryHandler<GetProjectQuery, ProjectReadModel>
|
||||
{
|
||||
constructor(
|
||||
private readonly findProjects: FindProjectsRepository,
|
||||
private readonly access: ReadAccess,
|
||||
) {}
|
||||
|
||||
async execute(query: GetProjectQuery): Promise<ProjectReadModel> {
|
||||
const project = UUID.test(query.idOrKey)
|
||||
? await this.findProjects.findById(query.idOrKey)
|
||||
: await this.findProjects.findByKey(query.idOrKey.toUpperCase());
|
||||
const found = this.access.assertFound(project, 'Project', query.idOrKey);
|
||||
await this.access.assertCanView(found.id, query.viewerId);
|
||||
return found;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { ProjectReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export class GetProjectQuery extends Query<ProjectReadModel> {
|
||||
constructor(
|
||||
public readonly viewerId: string,
|
||||
/** Project id or key ("TS"). */
|
||||
public readonly idOrKey: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { ReleaseProgressReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { FindIssuesRepository } from '../ports/find-issues.repository';
|
||||
import { FindReleasesRepository } from '../ports/find-releases.repository';
|
||||
import { ReadAccess } from './read-access';
|
||||
import { GetReleaseProgressQuery } from './get-release-progress.query';
|
||||
|
||||
@QueryHandler(GetReleaseProgressQuery)
|
||||
export class GetReleaseProgressQueryHandler
|
||||
implements IQueryHandler<GetReleaseProgressQuery, ReleaseProgressReadModel>
|
||||
{
|
||||
constructor(
|
||||
private readonly findReleases: FindReleasesRepository,
|
||||
private readonly findIssues: FindIssuesRepository,
|
||||
private readonly access: ReadAccess,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
query: GetReleaseProgressQuery,
|
||||
): Promise<ReleaseProgressReadModel> {
|
||||
const release = this.access.assertFound(
|
||||
await this.findReleases.findById(query.releaseId),
|
||||
'Release',
|
||||
query.releaseId,
|
||||
);
|
||||
await this.access.assertCanView(release.projectId, query.viewerId);
|
||||
return this.findIssues.releaseProgress(release.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { ReleaseProgressReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export class GetReleaseProgressQuery extends Query<ReleaseProgressReadModel> {
|
||||
constructor(
|
||||
public readonly viewerId: string,
|
||||
public readonly releaseId: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { ReleaseReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { FindReleasesRepository } from '../ports/find-releases.repository';
|
||||
import { ReadAccess } from './read-access';
|
||||
import { GetReleaseQuery } from './get-release.query';
|
||||
|
||||
@QueryHandler(GetReleaseQuery)
|
||||
export class GetReleaseQueryHandler
|
||||
implements IQueryHandler<GetReleaseQuery, ReleaseReadModel>
|
||||
{
|
||||
constructor(
|
||||
private readonly findReleases: FindReleasesRepository,
|
||||
private readonly access: ReadAccess,
|
||||
) {}
|
||||
|
||||
async execute(query: GetReleaseQuery): Promise<ReleaseReadModel> {
|
||||
const release = this.access.assertFound(
|
||||
await this.findReleases.findById(query.releaseId),
|
||||
'Release',
|
||||
query.releaseId,
|
||||
);
|
||||
await this.access.assertCanView(release.projectId, query.viewerId);
|
||||
return release;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { ReleaseReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export class GetReleaseQuery extends Query<ReleaseReadModel> {
|
||||
constructor(
|
||||
public readonly viewerId: string,
|
||||
public readonly releaseId: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { ReleaseReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { FindReleasesRepository } from '../ports/find-releases.repository';
|
||||
import { ReadAccess } from './read-access';
|
||||
import { GetReleasesQuery } from './get-releases.query';
|
||||
|
||||
@QueryHandler(GetReleasesQuery)
|
||||
export class GetReleasesQueryHandler
|
||||
implements IQueryHandler<GetReleasesQuery, ReleaseReadModel[]>
|
||||
{
|
||||
constructor(
|
||||
private readonly findReleases: FindReleasesRepository,
|
||||
private readonly access: ReadAccess,
|
||||
) {}
|
||||
|
||||
async execute(query: GetReleasesQuery): Promise<ReleaseReadModel[]> {
|
||||
await this.access.assertCanView(query.projectId, query.viewerId);
|
||||
return this.findReleases.findByProject(query.projectId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { ReleaseReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export class GetReleasesQuery extends Query<ReleaseReadModel[]> {
|
||||
constructor(
|
||||
public readonly viewerId: string,
|
||||
public readonly projectId: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
13
libs/api/tracker/application/src/lib/queries/index.ts
Normal file
13
libs/api/tracker/application/src/lib/queries/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export * from './can-view-project.query';
|
||||
export * from './get-activity.query';
|
||||
export * from './get-board.query';
|
||||
export * from './get-comments.query';
|
||||
export * from './get-issue.query';
|
||||
export * from './get-issues-by-ids.query';
|
||||
export * from './get-my-projects.query';
|
||||
export * from './get-project-stats.query';
|
||||
export * from './get-project.query';
|
||||
export * from './get-release-progress.query';
|
||||
export * from './get-release.query';
|
||||
export * from './get-releases.query';
|
||||
export * from './search-issues.query';
|
||||
@@ -0,0 +1,29 @@
|
||||
import { CanViewProjectQueryHandler } from './can-view-project.query-handler';
|
||||
import { GetActivityQueryHandler } from './get-activity.query-handler';
|
||||
import { GetBoardQueryHandler } from './get-board.query-handler';
|
||||
import { GetCommentsQueryHandler } from './get-comments.query-handler';
|
||||
import { GetIssueQueryHandler } from './get-issue.query-handler';
|
||||
import { GetIssuesByIdsQueryHandler } from './get-issues-by-ids.query-handler';
|
||||
import { GetMyProjectsQueryHandler } from './get-my-projects.query-handler';
|
||||
import { GetProjectQueryHandler } from './get-project.query-handler';
|
||||
import { GetProjectStatsQueryHandler } from './get-project-stats.query-handler';
|
||||
import { GetReleaseQueryHandler } from './get-release.query-handler';
|
||||
import { GetReleaseProgressQueryHandler } from './get-release-progress.query-handler';
|
||||
import { GetReleasesQueryHandler } from './get-releases.query-handler';
|
||||
import { SearchIssuesQueryHandler } from './search-issues.query-handler';
|
||||
|
||||
export const QUERY_HANDLERS = [
|
||||
CanViewProjectQueryHandler,
|
||||
GetActivityQueryHandler,
|
||||
GetBoardQueryHandler,
|
||||
GetCommentsQueryHandler,
|
||||
GetIssueQueryHandler,
|
||||
GetIssuesByIdsQueryHandler,
|
||||
GetMyProjectsQueryHandler,
|
||||
GetProjectQueryHandler,
|
||||
GetProjectStatsQueryHandler,
|
||||
GetReleaseQueryHandler,
|
||||
GetReleaseProgressQueryHandler,
|
||||
GetReleasesQueryHandler,
|
||||
SearchIssuesQueryHandler,
|
||||
];
|
||||
32
libs/api/tracker/application/src/lib/queries/read-access.ts
Normal file
32
libs/api/tracker/application/src/lib/queries/read-access.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { NotFoundError } from '@task-stream/api/shared/domain';
|
||||
import { ProjectRole } from '@task-stream/api/tracker/domain';
|
||||
import { FindProjectsRepository } from '../ports/find-projects.repository';
|
||||
|
||||
/** Authorisation for the read side, based on the projected memberships. */
|
||||
@Injectable()
|
||||
export class ReadAccess {
|
||||
constructor(private readonly findProjects: FindProjectsRepository) {}
|
||||
|
||||
async assertCanView(
|
||||
projectId: string,
|
||||
viewerId: string,
|
||||
): Promise<ProjectRole> {
|
||||
const role = await this.findProjects.findRole(projectId, viewerId);
|
||||
// Same answer for "doesn't exist" and "not yours": don't leak projects.
|
||||
if (!role) throw new NotFoundError('Project', projectId);
|
||||
return role;
|
||||
}
|
||||
|
||||
async canView(projectId: string, viewerId: string): Promise<boolean> {
|
||||
return (await this.findProjects.findRole(projectId, viewerId)) !== null;
|
||||
}
|
||||
|
||||
assertFound<T>(value: T | null, entity: string, id: string): T {
|
||||
if (!value) throw new NotFoundError(entity, id);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export const UUID =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { IssueReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { FindIssuesRepository, Page } from '../ports/find-issues.repository';
|
||||
import { ReadAccess } from './read-access';
|
||||
import { SearchIssuesQuery } from './search-issues.query';
|
||||
|
||||
@QueryHandler(SearchIssuesQuery)
|
||||
export class SearchIssuesQueryHandler
|
||||
implements IQueryHandler<SearchIssuesQuery, Page<IssueReadModel>>
|
||||
{
|
||||
constructor(
|
||||
private readonly findIssues: FindIssuesRepository,
|
||||
private readonly access: ReadAccess,
|
||||
) {}
|
||||
|
||||
async execute(query: SearchIssuesQuery): Promise<Page<IssueReadModel>> {
|
||||
await this.access.assertCanView(query.filter.projectId, query.viewerId);
|
||||
return this.findIssues.search({
|
||||
...query.filter,
|
||||
limit: Math.min(query.filter.limit, 200),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { IssueReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { IssueFilter, Page } from '../ports/find-issues.repository';
|
||||
|
||||
export class SearchIssuesQuery extends Query<Page<IssueReadModel>> {
|
||||
constructor(
|
||||
public readonly viewerId: string,
|
||||
public readonly filter: IssueFilter,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user