soft-delete-restore
Soft Delete Restore Pattern
Builds on: database-patterns (soft delete) and database-patterns (optimistic locking).
The BaseRepository already has a restore() method. This skill covers the full contract: repository, service, controller, authorization, and version behavior.
1. What Restore Does
Restore sets deletedAt = null on a soft-deleted record. It does not permanently undelete anything — records remain reversible either way.
Key invariants:
- Restore operates on a deleted record (
deletedAt IS NOT NULL). TheBaseRepository.restore()currently searches withdeletedAt: null— override this for deleted-record lookup. - Restore increments
versionwhen the model uses optimistic locking. This signals to stale clients that the record has changed. - Restore is a privileged write — apply role guards at the same level as delete or stricter.
2. Repository Layer
Override BaseRepository.restore() when the model uses optimistic locking, or to scope by studio.
// In TaskTemplateRepository
async restore(
params: { uid: string; studioUid?: string },
): Promise<TaskTemplate> {
const where: Prisma.TaskTemplateWhereUniqueInput = {
uid: params.uid,
...(params.studioUid && { studio: { uid: params.studioUid } }),
deletedAt: { not: null }, // Target deleted records only
};
return this.prisma.taskTemplate.update({
where,
data: {
deletedAt: null,
version: { increment: 1 }, // Signal record changed
},
});
}
Rule: Always scope where to deletedAt: { not: null }. A restore targeting an active record is a no-op at best, a data hazard at worst.
See
references/01-restore-repository.mdfor the full pattern including thefindDeletedhelper.
3. Service Layer
Convert PrismaClientKnownRequestError (P2025 record not found) to HttpError.notFound at the service boundary — never in the repository.
async restore(uid: string, studioUid?: string): Promise<TaskTemplate> {
try {
return await this.repository.restore({ uid, studioUid });
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === PRISMA_ERROR.RecordNotFound
) {
throw HttpError.notFound('Task template not found or not deleted');
}
throw error;
}
}
4. Controller Layer
Use a dedicated POST /:id/restore endpoint — not PATCH on the same resource, to keep restore intent explicit.
// Studio-scoped controller
@Post(':templateId/restore')
@ZodResponse(taskTemplateApiResponseSchema, HttpStatus.OK)
@StudioProtected([STUDIO_ROLE.ADMIN, STUDIO_ROLE.MANAGER])
async restore(
@Param('templateId', UidValidationPipe) templateUid: string,
@StudioParam() studioUid: string,
) {
const result = await this.service.restore(templateUid, studioUid);
return result;
}
Do not reuse the PATCH /:id endpoint to trigger restore — it conflates normal update with lifecycle transition.
5. Authorization Rules
Apply restore guards at the same level as delete, or stricter:
| Model | Delete role | Restore role |
|---|---|---|
| TaskTemplate | ADMIN, MANAGER | ADMIN, MANAGER (same) |
| Future models | Define at design time | Default: same as delete |
If a model has dependency constraints (e.g. a template with active tasks), restore must validate those before committing. Throw HttpError.conflict() from the service — not from the repository.
6. Dependency / Policy Constraints
Before restoring, check whether restore is safe:
async restore(uid: string, studioUid?: string): Promise<TaskTemplate> {
// Optional: check if a replacement record was created while this one was deleted
const conflict = await this.repository.findActiveByName(existingRecord.name, studioUid);
if (conflict) {
throw HttpError.conflict('A template with this name already exists.');
}
return this.repository.restore({ uid, studioUid });
}
Common checks:
- Uniqueness conflict: a replacement record with the same unique key was created while deleted.
- Dependency state: records that depended on this one have since been reassigned or removed.
- Role constraints: record belongs to a studio or scope that the caller no longer has access to.
7. Listing Deleted Records (Prerequisite for Restore UI)
Restore requires the client to know the deleted record exists. Add a findDeleted or include includeDeleted param to the list endpoint.
// Repository
async findPaginated(params: { includeDeleted?: boolean; ... }) {
const where = params.includeDeleted ? {} : { deletedAt: null };
// ...
}
Expose deleted_at in the API response so the frontend can show restore actions only on deleted records.
8. Version Behavior Summary
| Operation | Version change |
|---|---|
softDelete |
No increment (record is being hidden) |
restore |
Increment by 1 (record state changed) |
update |
Increment by 1 |
Clients holding a stale version after restore will receive 409 Conflict on their next version-guarded write, prompting a refresh. This is correct behavior.
Related Skills
- Database Patterns: Soft delete schema rules and
BaseRepository. - Database Patterns — Optimistic Locking: Version conflict error and service handling.
- Repository Pattern: Repository extension patterns.
- Backend Controller Pattern: Controller decorators and guards.
More from allenlin90/eridu-services
service-pattern-nestjs
Comprehensive NestJS service implementation patterns. This skill should be used when implementing Model Services, Orchestration Services, or business logic with NestJS decorators.
8erify-authorization
Patterns for implementing authorization in erify_api with current StudioMembership + AdminGuard behavior, plus planned RBAC references
6data-validation
Provides comprehensive guidance for input validation, data serialization, and ID management in backend APIs. This skill should be used when designing validation schemas, transforming request/response data, mapping database IDs to external identifiers, and ensuring type safety across API boundaries.
6repository-pattern-nestjs
Comprehensive Prisma repository implementation patterns for NestJS. This skill should be used when implementing repositories that extend BaseRepository or use Prisma delegates.
6task-template-builder
Provides guidelines for the Task Template Builder architecture, including Schema alignment, Draft storage, Drag-and-Drop, and Validation logic.
6schedule-continuity-workflow
Feature workflow for schedule planning continuity. Use when implementing or reviewing schedule update/validate/publish behavior, Google Sheets planning integration, and pending-resolution handling.
3