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