proper asset creation/deletion, also raw file view in admin as well

This commit is contained in:
user
2026-03-01 18:22:49 +02:00
parent 6c2b917088
commit 9716deead7
10 changed files with 524 additions and 69 deletions

View File

@@ -13,12 +13,14 @@ import { traceResultAsync } from "@core/observability";
import { MobileRepository } from "./repository";
import { settings } from "@core/settings";
import { mobileErrors } from "./errors";
import { errAsync } from "neverthrow";
import { getFileController, type FileController } from "@domains/files/controller";
import { errAsync, okAsync } from "neverthrow";
import { db } from "@pkg/db";
export class MobileController {
constructor(
private mobileRepo: MobileRepository,
private fileController: FileController,
private defaultAdminEmail?: string,
) {}
@@ -201,11 +203,13 @@ export class MobileController {
"app.mobile.media_asset_id": mediaAssetId,
},
fn: () =>
this.mobileRepo.deleteMediaAsset(
fctx,
mediaAssetId,
ownerUserId,
),
this.mobileRepo
.deleteMediaAsset(fctx, mediaAssetId, ownerUserId)
.andThen((fileId) =>
this.fileController
.deleteFiles(fctx, [fileId], ownerUserId)
.map(() => true),
),
});
}
@@ -217,7 +221,31 @@ export class MobileController {
"app.user.id": ownerUserId,
"app.mobile.device_id": deviceId,
},
fn: () => this.mobileRepo.deleteDevice(fctx, deviceId, ownerUserId),
fn: () =>
this.mobileRepo
.deleteDevice(fctx, deviceId, ownerUserId)
.andThen((result) => {
const cleanup = result.fileIds.length
? this.fileController.deleteFiles(
fctx,
result.fileIds,
ownerUserId,
)
: okAsync(true);
return cleanup.andThen(() =>
this.mobileRepo
.finalizeDeleteDevice(
fctx,
deviceId,
ownerUserId,
)
.map(() => ({
deleted: true,
deletedFileCount: result.fileIds.length,
})),
);
}),
});
}
@@ -235,6 +263,7 @@ export class MobileController {
export function getMobileController(): MobileController {
return new MobileController(
new MobileRepository(db),
getFileController(),
settings.defaultAdminEmail || undefined,
);
}

View File

@@ -5,11 +5,10 @@ import {
count,
desc,
eq,
inArray,
like,
or,
} from "@pkg/db";
import { file, mobileDevice, mobileMediaAsset, mobileSMS, user } from "@pkg/db/schema";
import { mobileDevice, mobileMediaAsset, mobileSMS, user } from "@pkg/db/schema";
import { ResultAsync, errAsync, okAsync } from "neverthrow";
import { FlowExecCtx } from "@core/flow.execution.context";
import type {
@@ -676,7 +675,7 @@ export class MobileRepository {
fctx: FlowExecCtx,
mediaAssetId: number,
ownerUserId: string,
): ResultAsync<boolean, Err> {
): ResultAsync<string, Err> {
const startedAt = Date.now();
return ResultAsync.fromPromise(
this.db
@@ -706,27 +705,32 @@ export class MobileRepository {
}
return ResultAsync.fromPromise(
this.db.transaction(async (tx) => {
await tx
.delete(mobileMediaAsset)
.where(eq(mobileMediaAsset.id, mediaAssetId));
await tx.delete(file).where(eq(file.id, target.fileId));
return true;
}),
this.db
.delete(mobileMediaAsset)
.where(eq(mobileMediaAsset.id, mediaAssetId))
.returning({ fileId: mobileMediaAsset.fileId }),
(error) =>
mobileErrors.deleteMediaFailed(
fctx,
error instanceof Error ? error.message : String(error),
),
);
}).map((deleted) => {
).andThen((deletedRows) => {
const deleted = deletedRows[0];
if (!deleted) {
return errAsync(
mobileErrors.mediaAssetNotFound(fctx, mediaAssetId),
);
}
return okAsync(deleted.fileId);
});
}).map((fileId) => {
logDomainEvent({
event: "mobile.media.delete.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { mediaAssetId, ownerUserId, deleted },
meta: { mediaAssetId, ownerUserId, fileId },
});
return deleted;
return fileId;
});
}
@@ -734,7 +738,7 @@ export class MobileRepository {
fctx: FlowExecCtx,
deviceId: number,
ownerUserId: string,
): ResultAsync<{ deleted: boolean; deletedFileCount: number }, Err> {
): ResultAsync<{ fileIds: string[] }, Err> {
const startedAt = Date.now();
return ResultAsync.fromPromise(
this.db
@@ -758,35 +762,54 @@ export class MobileRepository {
}
return ResultAsync.fromPromise(
this.db.transaction(async (tx) => {
const mediaFiles = await tx
.select({ fileId: mobileMediaAsset.fileId })
.from(mobileMediaAsset)
.where(eq(mobileMediaAsset.deviceId, deviceId));
const fileIds = mediaFiles.map((item) => item.fileId);
await tx.delete(mobileDevice).where(eq(mobileDevice.id, deviceId));
if (fileIds.length > 0) {
await tx.delete(file).where(inArray(file.id, fileIds));
}
return { deleted: true, deletedFileCount: fileIds.length };
}),
this.db
.select({ fileId: mobileMediaAsset.fileId })
.from(mobileMediaAsset)
.where(eq(mobileMediaAsset.deviceId, deviceId)),
(error) =>
mobileErrors.deleteDeviceFailed(
fctx,
error instanceof Error ? error.message : String(error),
),
);
).map((rows) => ({
fileIds: [...new Set(rows.map((item) => item.fileId))],
}));
}).map((result) => {
logDomainEvent({
event: "mobile.device.delete.succeeded",
event: "mobile.device.delete.prepared",
fctx,
durationMs: Date.now() - startedAt,
meta: { deviceId, deletedFileCount: result.deletedFileCount },
meta: { deviceId, deletedFileCount: result.fileIds.length },
});
return result;
});
}
finalizeDeleteDevice(
fctx: FlowExecCtx,
deviceId: number,
ownerUserId: string,
): ResultAsync<boolean, Err> {
return ResultAsync.fromPromise(
this.db
.delete(mobileDevice)
.where(
and(
eq(mobileDevice.id, deviceId),
eq(mobileDevice.ownerUserId, ownerUserId),
),
)
.returning({ id: mobileDevice.id }),
(error) =>
mobileErrors.deleteDeviceFailed(
fctx,
error instanceof Error ? error.message : String(error),
),
).andThen((rows) => {
if (!rows[0]) {
return errAsync(mobileErrors.deviceNotFoundById(fctx, deviceId));
}
return okAsync(true);
});
}
}

View File

@@ -13,6 +13,7 @@
"@pkg/keystore": "workspace:*",
"@pkg/logger": "workspace:*",
"@pkg/result": "workspace:*",
"@pkg/objectstorage ": "workspace:*",
"@pkg/settings": "workspace:*",
"@types/pdfkit": "^0.14.0",
"argon2": "^0.43.0",