Kind: Service
Source: atloria-monorepo/apps/api/src/change-request/change-request.service.ts
ChangeRequestService — DB lifecycle for docs change requests (S2.1).
A change request = a git branch (cr/open persists a 'draft' row carrying the proposed
content and enqueues a 'cr-open' job; CrWorker stages the branch and flips the row to 'open'
(markOpen) or 'failed' (markFailed).
ChangeRequestService manages the database lifecycle for documentation change requests and coordinates background work through the queue. API pods create draft records and enqueue work, while CrWorker performs Git operations on the worker-mounted repository and updates requests to open or failed.
Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
open | `open(projectId: string, dto: OpenChangeRequestDto, author: { authorType: string; authorId?: string | null })` | unknown |
list | list(projectId: string, status: string, take: unknown) | unknown | List a project's change requests, newest first, optionally filtered by status. |
get | get(projectId: string, crId: string) | unknown | One CR, scoped to the project (a crId from another project is a 404, not a leak). |
merge | merge(projectId: string, crId: string, mergedById: string) | unknown | Trigger the merge: ATOMIC open → 'merging' (the guard lives in the WHERE clause — a concurrent close/merge can never be clobbered), then enqueue the 'cr-merg… |
refreshDiff | refreshDiff(projectId: string, crId: string) | unknown | Recompute diffCache + conflictState against CURRENT main without merging anything — status stays untouched; the UI polls the row for the refreshed result. |
resolve | `resolve(projectId: string, crId: string, dto: ResolveChangeRequestDto, resolvedById: string | null)` | unknown |
close | close(projectId: string, crId: string) | unknown | Close without merging. |
markOpen | `markOpen(crId: string, data: { |
baseCommit: string | null; headCommit: string; files?: CrCommittedFile[]; diffCache?: CrDiff | null; previewFiles?: CrPreviewFile[]; })` | `unknown` | Worker hook: the branch is staged — record the commits, swap `files` to its committed shape ([{path, documentId}], content now lives in git), cache the diff,… |
| markFailed | markFailed(crId: string, error: string) | unknown | Worker hook: record a failure. |
| reclaimMerging | reclaimMerging(crId: string) | Promise<boolean> | Worker hook: a BullMQ retry re-driving a merge that failed mid-writeback reclaims the row 'failed' → 'merging'. |
| markMerged | markMerged(crId: string) | Promise<boolean> | Worker hook: writeback completed — 'merging' → 'merged' with mergedAt. |
| abortMergeWithConflicts | abortMergeWithConflicts(crId: string, conflictState: CrConflictState, error: string) | unknown | Worker hook: the conflict gate (or a mid-writeback CAS loss) found the merge unsafe — back to 'open' with the conflict report persisted. |
| markMergeFailed | markMergeFailed(crId: string, error: string) | unknown | Worker hook: merge failed mid-writeback — 'merging' → 'failed' (atomic; swallow-and-log like markFailed since this runs on error paths). |
| recordRefresh | recordRefresh(crId: string, diffCache: CrDiff, conflictState: CrConflictState | null) | unknown | Worker hook: a cr-refresh recompute landed — store it; status untouched. |
| recordRestage | recordRestage(crId: string, data: { baseCommit: string | null; headCommit: string; diffCache: CrDiff; conflictState: CrConflictState | null; previewFiles?: CrPreviewFile[]; }) | unknown | Worker hook: a cr-restage committed the resolution — the branch advanced (new base/head), the diff/conflict caches are fresh, and the pendingResolution is co… |
Dependencies
PrismaServiceDocsCrQueue
Where it refuses work
ChangeRequestServicestops the work withBadRequestExceptionwhenrow.status !== 'open', in 2 places.ChangeRequestServicestops the work withBadRequestExceptionwhentotalBytes >= MAX_PROPOSED_BYTES.ChangeRequestServicestops the work withBadRequestExceptionwhenfile.createPath || file.delete— “createPath/delete file entries are only valid on git-sync change requests”.ChangeRequestServicestops the work withBadRequestExceptionwhen!file.documentId— “each file needs a documentId”.ChangeRequestServicestops the work withBadRequestExceptionwhen!file.documentId— “a delete entry needs the documentId to unpublish”.ChangeRequestServicestops the work withBadRequestExceptionwhen!file.documentId && !file.createPath— “each git file entry needs a documentId or a createPath”.
When something fails
ChangeRequestServicehandles failure in 3 places: it turns it into a return value in 2, and discards it silently in 1. A failure discarded silently leaves no trace for whoever debugs this later.
Diagram
mermaidsequenceDiagram participant Client participant API as ChangeRequestService participant DB as DocsChangeRequest DB participant Queue participant Worker as CrWorker participant Git as Git Repository Client->>API: open(proposed content) API->>DB: Create request with status "draft" API->>Queue: Enqueue "cr-open" job API-->>Client: Return draft request Queue->>Worker: Process cr-open job Worker->>Git: Create branch cr/<id> and stage content alt Branch staged successfully Worker->>API: markOpen(id) API->>DB: Update status to "open" else Git operation failed Worker->>API: markFailed(id, error) API->>DB: Update status to "failed" end Client->>API: merge(), resolve(), or close() API->>DB: Update request lifecycle state
Usage
tsimport { ChangeRequestService } from './change-request.service';
@Injectable()
export class DocsController {
constructor(
private readonly changeRequests: ChangeRequestService,
) {}
async createChangeRequest(userId: string) {
const request = await this.changeRequests.open({
authorId: userId,
documentId: 'getting-started',
title: 'Clarify installation instructions',
content: '# Getting Started\n\nUpdated installation content.',
});
// The request is initially a draft while CrWorker creates cr/<id>.
return request;
}
async getOpenRequests() {
return this.changeRequests.list({
status: 'open',
});
}
async closeRequest(requestId: string, userId: string) {
return this.changeRequests.close(requestId, userId);
}
}
AI Coding Instructions
- Keep Git repository operations out of this service; API pods only persist request state and enqueue jobs.
- Treat
draftas the pre-worker state:open()must create the DB record before thecr-openjob is processed. - Use
markOpen()andmarkFailed()from worker flows after branch staging succeeds or fails; preserve failure details when available. - Respect lifecycle transitions when implementing
merge(),resolve(), andclose()so requests cannot be acted on from invalid states. - Use
reclaimMerging()for recovery of requests left in a merging state after interrupted worker or deployment activity.
Relationships
- DEPENDS_ON →
PrismaService - DEPENDS_ON →
DocsCrQueue
Referenced By
ChangeRequestController(DEPENDS_ON)ChangeRequestModule(MODULE_PROVIDES)ChangeRequestModule(MODULE_EXPORTS)DocAutomationService(DEPENDS_ON)GitSyncIngestService(DEPENDS_ON)
Was this page helpful?