added file domain logic, updated drizzle package
This commit is contained in:
287
packages/logic/domains/files/storage.repository.ts
Normal file
287
packages/logic/domains/files/storage.repository.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user