first commit
This commit is contained in:
59
libs/api/tracker/application/src/lib/ports/change-feed.ts
Normal file
59
libs/api/tracker/application/src/lib/ports/change-feed.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
ActivityReadModel,
|
||||
CommentReadModel,
|
||||
IssueReadModel,
|
||||
ProjectReadModel,
|
||||
ReleaseReadModel,
|
||||
} from '@task-stream/api/tracker/domain';
|
||||
|
||||
/** Every realtime message says which event caused it and the resulting version. */
|
||||
export interface Change {
|
||||
/** Event class name, e.g. `IssueStatusChangedEvent`. */
|
||||
action: string;
|
||||
version: number;
|
||||
actorId: string;
|
||||
}
|
||||
|
||||
export interface IssueChange extends Change {
|
||||
issue: IssueReadModel;
|
||||
}
|
||||
|
||||
export interface CommentChange extends Change {
|
||||
issueId: string;
|
||||
commentId: string;
|
||||
/** Null when the comment was deleted. */
|
||||
comment: CommentReadModel | null;
|
||||
}
|
||||
|
||||
export interface ReleaseChange extends Change {
|
||||
release: ReleaseReadModel;
|
||||
}
|
||||
|
||||
export interface ProjectChange extends Change {
|
||||
project: ProjectReadModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Live stream of read-model changes: event handlers publish once the read
|
||||
* model is up to date, GraphQL subscriptions watch.
|
||||
*/
|
||||
export abstract class ChangeFeed {
|
||||
abstract publishIssue(change: IssueChange): Promise<void>;
|
||||
abstract publishComment(change: CommentChange): Promise<void>;
|
||||
abstract publishActivity(activity: ActivityReadModel): Promise<void>;
|
||||
abstract publishRelease(change: ReleaseChange): Promise<void>;
|
||||
/** @param notifyUserIds users whose project list changed (joined / left). */
|
||||
abstract publishProject(
|
||||
change: ProjectChange,
|
||||
notifyUserIds?: string[],
|
||||
): Promise<void>;
|
||||
|
||||
abstract issues(projectId: string): AsyncIterableIterator<IssueChange>;
|
||||
abstract comments(issueId: string): AsyncIterableIterator<CommentChange>;
|
||||
abstract activity(
|
||||
projectId: string,
|
||||
): AsyncIterableIterator<ActivityReadModel>;
|
||||
abstract releases(projectId: string): AsyncIterableIterator<ReleaseChange>;
|
||||
abstract project(projectId: string): AsyncIterableIterator<ProjectChange>;
|
||||
abstract userProjects(userId: string): AsyncIterableIterator<ProjectChange>;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ActivityReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export abstract class FindActivityRepository {
|
||||
abstract find(params: {
|
||||
projectId: string;
|
||||
issueId?: string;
|
||||
limit: number;
|
||||
/** ISO date cursor: only activity strictly older than this. */
|
||||
before?: string;
|
||||
}): Promise<ActivityReadModel[]>;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CommentReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { Page } from './find-issues.repository';
|
||||
|
||||
export abstract class FindCommentsRepository {
|
||||
abstract findById(id: string): Promise<CommentReadModel | null>;
|
||||
abstract findByIssue(
|
||||
issueId: string,
|
||||
limit: number,
|
||||
offset: number,
|
||||
): Promise<Page<CommentReadModel>>;
|
||||
abstract countByIssue(issueId: string): Promise<number>;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
IssueArea,
|
||||
IssuePriority,
|
||||
IssueReadModel,
|
||||
IssueStatus,
|
||||
IssueType,
|
||||
ProjectStatsReadModel,
|
||||
ReleaseProgressReadModel,
|
||||
} from '@task-stream/api/tracker/domain';
|
||||
|
||||
export interface IssueFilter {
|
||||
projectId: string;
|
||||
statuses?: IssueStatus[];
|
||||
types?: IssueType[];
|
||||
areas?: IssueArea[];
|
||||
priorities?: IssuePriority[];
|
||||
assigneeId?: string;
|
||||
unassigned?: boolean;
|
||||
releaseId?: string;
|
||||
labels?: string[];
|
||||
search?: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface Page<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export abstract class FindIssuesRepository {
|
||||
abstract findById(id: string): Promise<IssueReadModel | null>;
|
||||
abstract findByKey(key: string): Promise<IssueReadModel | null>;
|
||||
abstract findByIds(ids: string[]): Promise<IssueReadModel[]>;
|
||||
abstract search(filter: IssueFilter): Promise<Page<IssueReadModel>>;
|
||||
abstract stats(projectId: string): Promise<ProjectStatsReadModel>;
|
||||
abstract releaseProgress(
|
||||
releaseId: string,
|
||||
): Promise<ReleaseProgressReadModel>;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ProjectReadModel, ProjectRole } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export abstract class FindProjectsRepository {
|
||||
abstract findById(id: string): Promise<ProjectReadModel | null>;
|
||||
abstract findByKey(key: string): Promise<ProjectReadModel | null>;
|
||||
abstract findByMember(userId: string): Promise<ProjectReadModel[]>;
|
||||
abstract findRole(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
): Promise<ProjectRole | null>;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ReleaseReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export abstract class FindReleasesRepository {
|
||||
abstract findById(id: string): Promise<ReleaseReadModel | null>;
|
||||
abstract findByProject(projectId: string): Promise<ReleaseReadModel[]>;
|
||||
}
|
||||
16
libs/api/tracker/application/src/lib/ports/index.ts
Normal file
16
libs/api/tracker/application/src/lib/ports/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export * from './project-keys.repository';
|
||||
export * from './issue-numbers.repository';
|
||||
export * from './release-names.repository';
|
||||
export * from './find-projects.repository';
|
||||
export * from './find-issues.repository';
|
||||
export * from './find-comments.repository';
|
||||
export * from './find-releases.repository';
|
||||
export * from './find-activity.repository';
|
||||
export * from './upsert-materialized-project.repository';
|
||||
export * from './upsert-materialized-issue.repository';
|
||||
export * from './upsert-materialized-comment.repository';
|
||||
export * from './upsert-materialized-release.repository';
|
||||
export * from './upsert-materialized-activity.repository';
|
||||
export * from './change-feed';
|
||||
export * from './user-directory';
|
||||
export * from './release-issues.repository';
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Per-project ticket counter (TS-1, TS-2...). */
|
||||
export abstract class IssueNumbersRepository {
|
||||
/** Atomically reserves the next number of the project. */
|
||||
abstract next(projectId: string): Promise<number>;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Write-side uniqueness of project keys ("TS", "SHOP"...). */
|
||||
export abstract class ProjectKeysRepository {
|
||||
/** Throws BusinessRuleError if the key is already taken. */
|
||||
abstract reserve(key: string, projectId: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Write-side lookup over the event store: tickets that were ever put in a
|
||||
* release. Rules about a release (e.g. "no unfinished tickets") must not
|
||||
* depend on the eventually-consistent read model.
|
||||
*/
|
||||
export abstract class ReleaseIssuesRepository {
|
||||
abstract findIssueIds(releaseId: string): Promise<string[]>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/** Write-side uniqueness of release names inside a project. */
|
||||
export abstract class ReleaseNamesRepository {
|
||||
/** Throws BusinessRuleError if the name is already used in the project. */
|
||||
abstract reserve(
|
||||
projectId: string,
|
||||
name: string,
|
||||
releaseId: string,
|
||||
): Promise<void>;
|
||||
abstract free(projectId: string, name: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ActivityReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export abstract class UpsertMaterializedActivityRepository {
|
||||
/** Inserts the entry unless it already exists. Returns true when inserted. */
|
||||
abstract insert(activity: ActivityReadModel): Promise<boolean>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CommentReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export abstract class UpsertMaterializedCommentRepository {
|
||||
/** Applied only if `version` is newer than the stored one (idempotent). */
|
||||
abstract upsert(
|
||||
comment: Pick<CommentReadModel, 'id' | 'version'> &
|
||||
Partial<CommentReadModel>,
|
||||
): Promise<void>;
|
||||
abstract remove(id: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
IssueLinkReadModel,
|
||||
IssueReadModel,
|
||||
} from '@task-stream/api/tracker/domain';
|
||||
|
||||
export abstract class UpsertMaterializedIssueRepository {
|
||||
/** Applied only if `version` is newer than the stored one (idempotent). */
|
||||
abstract upsert(
|
||||
issue: Pick<IssueReadModel, 'id' | 'version'> &
|
||||
Partial<Omit<IssueReadModel, 'links' | 'commentCount'>>,
|
||||
): Promise<void>;
|
||||
/** Adds a relation to an issue (idempotent). */
|
||||
abstract addLink(issueId: string, link: IssueLinkReadModel): Promise<void>;
|
||||
abstract removeLink(
|
||||
issueId: string,
|
||||
link: Omit<IssueLinkReadModel, 'issueKey'>,
|
||||
): Promise<void>;
|
||||
abstract setCommentCount(
|
||||
issueId: string,
|
||||
commentCount: number,
|
||||
): Promise<void>;
|
||||
/** Keeps the denormalised release name of every issue in sync. */
|
||||
abstract renameRelease(releaseId: string, name: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ProjectReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export abstract class UpsertMaterializedProjectRepository {
|
||||
/** Applied only if `version` is newer than the stored one (idempotent). */
|
||||
abstract upsert(
|
||||
project: Pick<ProjectReadModel, 'id' | 'version'> &
|
||||
Partial<ProjectReadModel>,
|
||||
): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ReleaseReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export abstract class UpsertMaterializedReleaseRepository {
|
||||
/** Applied only if `version` is newer than the stored one (idempotent). */
|
||||
abstract upsert(
|
||||
release: Pick<ReleaseReadModel, 'id' | 'version'> &
|
||||
Partial<ReleaseReadModel>,
|
||||
): Promise<void>;
|
||||
}
|
||||
28
libs/api/tracker/application/src/lib/ports/user-directory.ts
Normal file
28
libs/api/tracker/application/src/lib/ports/user-directory.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { UserRefReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
export interface DirectoryUser {
|
||||
id: string;
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
/** Outbound port to the identity context. */
|
||||
export abstract class UserDirectory {
|
||||
/**
|
||||
* @param accessToken caller's token, lets the identity server be asked for
|
||||
* users that never used task-stream yet.
|
||||
*/
|
||||
abstract findById(
|
||||
userId: string,
|
||||
accessToken?: string,
|
||||
): Promise<DirectoryUser | null>;
|
||||
|
||||
/** Resolves user ids to {id, name} for read models, in one lookup. */
|
||||
abstract refs(userIds: (string | null | undefined)[]): Promise<UserRefs>;
|
||||
}
|
||||
|
||||
export interface UserRefs {
|
||||
/** Unknown users keep their id as name. */
|
||||
of(userId: string): UserRefReadModel;
|
||||
ofNullable(userId: string | null | undefined): UserRefReadModel | null;
|
||||
}
|
||||
Reference in New Issue
Block a user