added file domain logic, updated drizzle package

This commit is contained in:
user
2026-03-01 05:56:15 +02:00
parent 1c2584df58
commit 5a5f565377
27 changed files with 5757 additions and 223 deletions

View File

@@ -0,0 +1,287 @@
import { ResultAsync, errAsync, okAsync } from "neverthrow";
import { FlowExecCtx } from "@core/flow.execution.context";
import type { PresignedUploadResponse } from "./data";
import { R2StorageClient } from "@pkg/objectstorage";
import { type Err } from "@pkg/result";
import { fileErrors } from "./errors";
import { logDomainEvent } from "@pkg/logger";
export type StorageConfig = {
bucketName: string;
region: string;
endpoint: string;
accessKey: string;
secretKey: string;
publicUrl: string;
maxFileSize: number;
allowedMimeTypes: string[];
allowedExtensions: string[];
};
export type UploadOptions = {
visibility: "public" | "private";
metadata?: Record<string, any>;
tags?: string[];
processImage?: boolean;
processDocument?: boolean;
processVideo?: boolean;
};
export type UploadedFileMetadata = {
id: string;
filename: string;
originalName: string;
mimeType: string;
size: number;
hash: string;
bucketName: string;
objectKey: string;
r2Url: string;
visibility: string;
userId: string;
metadata?: Record<string, any>;
tags?: string[];
uploadedAt: Date;
};
export class StorageRepository {
private storageClient: R2StorageClient;
constructor(config: StorageConfig) {
this.storageClient = new R2StorageClient(config);
}
uploadFile(
fctx: FlowExecCtx,
buffer: Buffer,
filename: string,
mimeType: string,
userId: string,
options: UploadOptions,
): ResultAsync<UploadedFileMetadata, Err> {
const startedAt = Date.now();
logDomainEvent({
event: "files.storage.upload.started",
fctx,
meta: {
userId,
filename,
mimeType,
size: buffer.length,
visibility: options.visibility,
},
});
return ResultAsync.fromPromise(
this.storageClient.uploadFile(
buffer,
filename,
mimeType,
userId,
options,
),
(error) => {
logDomainEvent({
level: "error",
event: "files.storage.upload.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { userId, filename },
});
return fileErrors.uploadFailed(
fctx,
error instanceof Error ? error.message : String(error),
);
},
).andThen((uploadResult) => {
if (uploadResult.error) {
logDomainEvent({
level: "error",
event: "files.storage.upload.failed",
fctx,
durationMs: Date.now() - startedAt,
error: uploadResult.error,
meta: { userId, filename, stage: "storage_response" },
});
return errAsync(
fileErrors.uploadFailed(fctx, String(uploadResult.error)),
);
}
const uploadData = uploadResult.data;
if (!uploadData || !uploadData.file) {
logDomainEvent({
level: "error",
event: "files.storage.upload.failed",
fctx,
durationMs: Date.now() - startedAt,
error: {
code: "NO_FILE_METADATA",
message: "Storage upload returned no file metadata",
},
meta: { userId, filename },
});
return errAsync(fileErrors.noFileMetadata(fctx));
}
logDomainEvent({
event: "files.storage.upload.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { userId, fileId: uploadData.file.id, filename },
});
return okAsync(uploadData.file as UploadedFileMetadata);
});
}
generatePresignedUploadUrl(
fctx: FlowExecCtx,
objectKey: string,
mimeType: string,
expiresIn: number,
): ResultAsync<PresignedUploadResponse, Err> {
const startedAt = Date.now();
logDomainEvent({
event: "files.storage.presigned.started",
fctx,
meta: { objectKey, mimeType, expiresIn },
});
return ResultAsync.fromPromise(
this.storageClient.generatePresignedUploadUrl(
objectKey,
mimeType,
expiresIn,
),
(error) => {
logDomainEvent({
level: "error",
event: "files.storage.presigned.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { objectKey },
});
return fileErrors.presignedUrlFailed(
fctx,
error instanceof Error ? error.message : String(error),
);
},
).andThen((result) => {
if (result.error) {
logDomainEvent({
level: "error",
event: "files.storage.presigned.failed",
fctx,
durationMs: Date.now() - startedAt,
error: result.error,
meta: { objectKey, stage: "storage_response" },
});
return errAsync(
fileErrors.presignedUrlFailed(fctx, String(result.error)),
);
}
const presignedData = result.data;
if (!presignedData) {
logDomainEvent({
level: "error",
event: "files.storage.presigned.failed",
fctx,
durationMs: Date.now() - startedAt,
error: {
code: "NO_PRESIGNED_DATA",
message: "No presigned data returned",
},
meta: { objectKey },
});
return errAsync(fileErrors.noPresignedData(fctx));
}
logDomainEvent({
event: "files.storage.presigned.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { objectKey },
});
return okAsync(presignedData as PresignedUploadResponse);
});
}
deleteFile(
fctx: FlowExecCtx,
objectKey: string,
): ResultAsync<boolean, Err> {
const startedAt = Date.now();
logDomainEvent({
event: "files.storage.delete.started",
fctx,
meta: { objectKey },
});
return ResultAsync.fromPromise(
this.storageClient.deleteFile(objectKey),
(error) => {
logDomainEvent({
level: "error",
event: "files.storage.delete.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { objectKey },
});
return fileErrors.storageError(
fctx,
error instanceof Error ? error.message : String(error),
);
},
).andThen((result) => {
if (result.error) {
logDomainEvent({
level: "error",
event: "files.storage.delete.failed",
fctx,
durationMs: Date.now() - startedAt,
error: result.error,
meta: { objectKey, stage: "storage_response" },
});
return errAsync(
fileErrors.storageError(fctx, String(result.error)),
);
}
logDomainEvent({
event: "files.storage.delete.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { objectKey },
});
return okAsync(true);
});
}
deleteFiles(
fctx: FlowExecCtx,
objectKeys: string[],
): ResultAsync<boolean, Err> {
const startedAt = Date.now();
logDomainEvent({
event: "files.storage.delete_many.started",
fctx,
meta: { fileCount: objectKeys.length },
});
return ResultAsync.combine(
objectKeys.map((key) => this.deleteFile(fctx, key)),
).map(() => {
logDomainEvent({
event: "files.storage.delete_many.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { fileCount: objectKeys.length },
});
return true;
});
}
}