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,537 @@
import type {
File,
FileFilters,
FileShareRequest,
FileUpdateRequest,
PaginatedFiles,
PaginationOptions,
} from "./data";
import {
Database,
and,
asc,
count,
desc,
eq,
inArray,
like,
or,
sql,
} from "@pkg/db";
import { ResultAsync, errAsync, okAsync } from "neverthrow";
import { FlowExecCtx } from "@core/flow.execution.context";
import { file, fileAccess } from "@pkg/db/schema";
import { type Err } from "@pkg/result";
import { fileErrors } from "./errors";
import { logDomainEvent } from "@pkg/logger";
export class FileRepository {
constructor(private db: Database) {}
getFiles(
fctx: FlowExecCtx,
filters: FileFilters,
pagination: PaginationOptions,
): ResultAsync<PaginatedFiles, Err> {
const startedAt = Date.now();
logDomainEvent({
event: "files.list.started",
fctx,
meta: {
userId: filters.userId,
hasSearch: Boolean(filters.search),
hasTags: Boolean(filters.tags?.length),
page: pagination.page,
pageSize: pagination.pageSize,
},
});
const { userId, mimeType, visibility, status, search, tags } = filters;
const {
page,
pageSize,
sortBy = "createdAt",
sortOrder = "desc",
} = pagination;
const conditions = [eq(file.userId, userId)];
if (mimeType) {
conditions.push(like(file.mimeType, `${mimeType}%`));
}
if (visibility) {
conditions.push(eq(file.visibility, visibility));
}
if (status) {
conditions.push(eq(file.status, status));
}
if (search) {
conditions.push(
or(
like(file.filename, `%${search}%`),
like(file.originalName, `%${search}%`),
)!,
);
}
if (tags && tags.length > 0) {
conditions.push(sql`${file.tags} @> ${JSON.stringify(tags)}`);
}
const whereClause = and(...conditions);
return ResultAsync.fromPromise(
this.db.select({ count: count() }).from(file).where(whereClause),
(error) => {
logDomainEvent({
level: "error",
event: "files.list.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { userId },
});
return fileErrors.getFilesFailed(
fctx,
error instanceof Error ? error.message : String(error),
);
},
).andThen((totalResult) => {
const total = totalResult[0]?.count || 0;
const offset = (page - 1) * pageSize;
const getOrderColumn = (currentSortBy: string) => {
switch (currentSortBy) {
case "createdAt":
return file.createdAt;
case "uploadedAt":
return file.uploadedAt;
case "size":
return file.size;
case "filename":
return file.filename;
default:
return file.createdAt;
}
};
const orderColumn = getOrderColumn(sortBy);
const orderFunc = sortOrder === "asc" ? asc : desc;
return ResultAsync.fromPromise(
this.db
.select()
.from(file)
.where(whereClause)
.orderBy(orderFunc(orderColumn))
.limit(pageSize)
.offset(offset),
(error) => {
logDomainEvent({
level: "error",
event: "files.list.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { userId },
});
return fileErrors.getFilesFailed(
fctx,
error instanceof Error ? error.message : String(error),
);
},
).map((data) => {
const totalPages = Math.ceil(total / pageSize);
logDomainEvent({
event: "files.list.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: {
userId,
page,
totalPages,
count: data.length,
},
});
return {
data: data as File[],
total,
page,
pageSize,
totalPages,
};
});
});
}
getFileById(
fctx: FlowExecCtx,
fileId: string,
userId: string,
): ResultAsync<File, Err> {
const startedAt = Date.now();
logDomainEvent({
event: "files.get.started",
fctx,
meta: { fileId, userId },
});
return ResultAsync.fromPromise(
this.db
.select()
.from(file)
.where(and(eq(file.id, fileId), eq(file.userId, userId)))
.limit(1),
(error) => {
logDomainEvent({
level: "error",
event: "files.get.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { fileId, userId },
});
return fileErrors.getFileFailed(
fctx,
error instanceof Error ? error.message : String(error),
);
},
).andThen((result) => {
const dbFile = result[0];
if (!dbFile) {
logDomainEvent({
level: "warn",
event: "files.get.failed",
fctx,
durationMs: Date.now() - startedAt,
error: { code: "NOT_FOUND", message: "File not found" },
meta: { fileId, userId },
});
return errAsync(fileErrors.fileNotFound(fctx, fileId));
}
logDomainEvent({
event: "files.get.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { fileId, userId },
});
return okAsync(dbFile as File);
});
}
createFile(
fctx: FlowExecCtx,
fileData: Omit<File, "createdAt" | "updatedAt">,
): ResultAsync<File, Err> {
const startedAt = Date.now();
logDomainEvent({
event: "files.create.started",
fctx,
meta: { userId: fileData.userId, filename: fileData.filename },
});
const now = new Date();
const insertData = {
...fileData,
createdAt: now,
updatedAt: now,
} as any;
return ResultAsync.fromPromise(
this.db.insert(file).values(insertData).returning(),
(error) => {
logDomainEvent({
level: "error",
event: "files.create.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { userId: fileData.userId, filename: fileData.filename },
});
return fileErrors.createFileFailed(
fctx,
error instanceof Error ? error.message : String(error),
);
},
).map((result) => {
const created = result[0] as File;
logDomainEvent({
event: "files.create.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { fileId: created.id, userId: created.userId },
});
return created;
});
}
updateFile(
fctx: FlowExecCtx,
fileId: string,
userId: string,
updates: FileUpdateRequest,
): ResultAsync<File, Err> {
const startedAt = Date.now();
logDomainEvent({
event: "files.update.started",
fctx,
meta: {
fileId,
userId,
hasFilename: updates.filename !== undefined,
hasMetadata: updates.metadata !== undefined,
hasTags: updates.tags !== undefined,
},
});
const updateData = {
...updates,
updatedAt: new Date(),
} as any;
return ResultAsync.fromPromise(
this.db
.update(file)
.set(updateData)
.where(and(eq(file.id, fileId), eq(file.userId, userId)))
.returning(),
(error) => {
logDomainEvent({
level: "error",
event: "files.update.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { fileId, userId },
});
return fileErrors.updateFileFailed(
fctx,
error instanceof Error ? error.message : String(error),
);
},
).andThen((result) => {
const updated = result[0];
if (!updated) {
logDomainEvent({
level: "warn",
event: "files.update.failed",
fctx,
durationMs: Date.now() - startedAt,
error: { code: "NOT_FOUND", message: "File not found" },
meta: { fileId, userId },
});
return errAsync(fileErrors.fileNotFound(fctx, fileId));
}
logDomainEvent({
event: "files.update.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { fileId, userId },
});
return okAsync(updated as File);
});
}
deleteFiles(
fctx: FlowExecCtx,
fileIds: readonly string[],
userId: string,
): ResultAsync<boolean, Err> {
const startedAt = Date.now();
logDomainEvent({
event: "files.delete.started",
fctx,
meta: { userId, fileCount: fileIds.length },
});
return ResultAsync.fromPromise(
this.db
.delete(file)
.where(and(eq(file.userId, userId), inArray(file.id, [...fileIds]))),
(error) => {
logDomainEvent({
level: "error",
event: "files.delete.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { userId, fileCount: fileIds.length },
});
return fileErrors.deleteFilesFailed(
fctx,
error instanceof Error ? error.message : String(error),
);
},
).map(() => {
logDomainEvent({
event: "files.delete.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { userId, fileCount: fileIds.length },
});
return true;
});
}
updateFileStatus(
fctx: FlowExecCtx,
fileId: string,
status: string,
processingError?: string,
): ResultAsync<boolean, Err> {
const startedAt = Date.now();
logDomainEvent({
event: "files.status_update.started",
fctx,
meta: {
fileId,
status,
hasProcessingError: Boolean(processingError),
},
});
return ResultAsync.fromPromise(
this.db
.update(file)
.set({
status,
processingError,
updatedAt: new Date(),
})
.where(eq(file.id, fileId)),
(error) => {
logDomainEvent({
level: "error",
event: "files.status_update.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { fileId, status },
});
return fileErrors.updateStatusFailed(
fctx,
error instanceof Error ? error.message : String(error),
);
},
).map(() => {
logDomainEvent({
event: "files.status_update.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { fileId, status },
});
return true;
});
}
shareFile(
fctx: FlowExecCtx,
fileId: string,
ownerId: string,
shareRequest: FileShareRequest,
): ResultAsync<boolean, Err> {
const startedAt = Date.now();
logDomainEvent({
event: "files.share.started",
fctx,
meta: {
fileId,
ownerId,
targetUserId: shareRequest.userId,
},
});
return ResultAsync.fromPromise(
this.db
.select()
.from(file)
.where(and(eq(file.id, fileId), eq(file.userId, ownerId)))
.limit(1),
(error) => {
logDomainEvent({
level: "error",
event: "files.share.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: { fileId, ownerId, targetUserId: shareRequest.userId },
});
return fileErrors.shareFileFailed(
fctx,
error instanceof Error ? error.message : String(error),
);
},
).andThen((result) => {
const ownedFile = result[0];
if (!ownedFile) {
logDomainEvent({
level: "warn",
event: "files.share.failed",
fctx,
durationMs: Date.now() - startedAt,
error: {
code: "NOT_FOUND",
message: "File not found or not owned by user",
},
meta: { fileId, ownerId, targetUserId: shareRequest.userId },
});
return errAsync(fileErrors.fileNotFound(fctx, fileId));
}
const now = new Date();
return ResultAsync.fromPromise(
this.db
.insert(fileAccess)
.values({
fileId,
userId: shareRequest.userId,
canRead: shareRequest.permissions.canRead || false,
canWrite: shareRequest.permissions.canWrite || false,
canDelete: shareRequest.permissions.canDelete || false,
canShare: shareRequest.permissions.canShare || false,
grantedAt: now,
expiresAt: shareRequest.expiresAt,
createdAt: now,
updatedAt: now,
})
.onConflictDoNothing(),
(error) => {
logDomainEvent({
level: "error",
event: "files.share.failed",
fctx,
durationMs: Date.now() - startedAt,
error,
meta: {
fileId,
ownerId,
targetUserId: shareRequest.userId,
},
});
return fileErrors.shareFileFailed(
fctx,
error instanceof Error ? error.message : String(error),
);
},
).map(() => {
logDomainEvent({
event: "files.share.succeeded",
fctx,
durationMs: Date.now() - startedAt,
meta: { fileId, ownerId, targetUserId: shareRequest.userId },
});
return true;
});
});
}
}