feat(documents): block rename/move/delete on system folders
assertNotSystemManaged centralises the guard so the three mutation paths surface identical ConflictError shapes. System roots and per- entity subfolders are immutable through the rep-facing API; the only way for system_managed to flip back to false is the entity-hard- delete demotion path (next task). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,26 @@ function isSiblingNameConflict(err: unknown): boolean {
|
||||
return constraint === 'uniq_document_folders_sibling_name';
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws ConflictError if the folder is system-managed. Centralises the
|
||||
* rejection so rename/move/delete all surface identical error shapes.
|
||||
*/
|
||||
async function assertNotSystemManaged(
|
||||
portId: string,
|
||||
folderId: string,
|
||||
action: 'rename' | 'move' | 'delete',
|
||||
): Promise<DocumentFolder> {
|
||||
const folder = await db.query.documentFolders.findFirst({
|
||||
where: and(eq(documentFolders.id, folderId), eq(documentFolders.portId, portId)),
|
||||
});
|
||||
if (!folder) throw new NotFoundError('Folder');
|
||||
if (folder.systemManaged) {
|
||||
const verb = action === 'rename' ? 'renamed' : action === 'move' ? 'moved' : 'deleted';
|
||||
throw new ConflictError(`System folders can't be ${verb}`);
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
export interface FolderNode extends DocumentFolder {
|
||||
children: FolderNode[];
|
||||
}
|
||||
@@ -130,10 +150,7 @@ export async function renameFolder(
|
||||
if (!trimmed) throw new ValidationError('Folder name cannot be empty');
|
||||
if (trimmed.length > 200) throw new ValidationError('Folder name cannot exceed 200 chars');
|
||||
|
||||
const existing = await db.query.documentFolders.findFirst({
|
||||
where: and(eq(documentFolders.id, folderId), eq(documentFolders.portId, portId)),
|
||||
});
|
||||
if (!existing) throw new NotFoundError('Folder');
|
||||
const existing = await assertNotSystemManaged(portId, folderId, 'rename');
|
||||
|
||||
try {
|
||||
const [updated] = await db
|
||||
@@ -178,10 +195,7 @@ export async function moveFolder(
|
||||
throw new ValidationError('Cannot move a folder under itself (cycle)');
|
||||
}
|
||||
|
||||
const folder = await db.query.documentFolders.findFirst({
|
||||
where: and(eq(documentFolders.id, folderId), eq(documentFolders.portId, portId)),
|
||||
});
|
||||
if (!folder) throw new NotFoundError('Folder');
|
||||
const folder = await assertNotSystemManaged(portId, folderId, 'move');
|
||||
|
||||
if (newParentId !== null) {
|
||||
const newParent = await db.query.documentFolders.findFirst({
|
||||
@@ -247,10 +261,7 @@ export async function deleteFolderSoftRescue(
|
||||
folderId: string,
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
const folder = await db.query.documentFolders.findFirst({
|
||||
where: and(eq(documentFolders.id, folderId), eq(documentFolders.portId, portId)),
|
||||
});
|
||||
if (!folder) throw new NotFoundError('Folder');
|
||||
const folder = await assertNotSystemManaged(portId, folderId, 'delete');
|
||||
|
||||
const newParent = folder.parentId; // null = re-parent to root
|
||||
|
||||
|
||||
Reference in New Issue
Block a user