@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
198 lines (165 loc) • 6.19 kB
text/typescript
import debug from 'debug';
import { and, eq } from 'drizzle-orm';
import { LobeChatDatabase, Transaction } from '@/database/type';
import { FileService } from '@/server/services/file';
import { AsyncTaskError, AsyncTaskStatus } from '@/types/asyncTask';
import { FileSource } from '@/types/files';
import { Generation, ImageGenerationAsset } from '@/types/generation';
import { NewFile } from '../schemas';
import {
GenerationItem,
GenerationWithAsyncTask,
NewGeneration,
generations,
} from '../schemas/generation';
import { FileModel } from './file';
// Create debug logger
const log = debug('lobe-image:generation-model');
export class GenerationModel {
private db: LobeChatDatabase;
private userId: string;
private fileModel: FileModel;
private fileService: FileService;
constructor(db: LobeChatDatabase, userId: string) {
this.db = db;
this.userId = userId;
this.fileModel = new FileModel(db, userId);
this.fileService = new FileService(db, userId);
}
async create(value: Omit<NewGeneration, 'userId'>): Promise<GenerationItem> {
log('Creating generation: %O', {
generationBatchId: value.generationBatchId,
userId: this.userId,
});
const [result] = await this.db
.insert(generations)
.values({ ...value, userId: this.userId })
.returning();
log('Generation created successfully: %s', result.id);
return result;
}
async findById(id: string): Promise<GenerationItem | undefined> {
log('Finding generation by ID: %s for user: %s', id, this.userId);
const result = await this.db.query.generations.findFirst({
where: and(eq(generations.id, id), eq(generations.userId, this.userId)),
});
log('Generation %s: %s', id, result ? 'found' : 'not found');
return result;
}
async findByIdWithAsyncTask(id: string): Promise<GenerationWithAsyncTask | undefined> {
log('Finding generation by ID: %s for user: %s', id, this.userId);
const result = await this.db.query.generations.findFirst({
where: and(eq(generations.id, id), eq(generations.userId, this.userId)),
with: {
asyncTask: true,
},
});
log('Generation %s: %s', id, result ? 'found' : 'not found');
return result as GenerationWithAsyncTask | undefined;
}
async update(id: string, value: Partial<NewGeneration>, trx?: Transaction) {
log('Updating generation: %s with values: %O', id, {
asyncTaskId: value.asyncTaskId,
hasAsset: !!value.asset,
});
const executeUpdate = async (tx: Transaction) => {
return await tx
.update(generations)
.set({ ...value, updatedAt: new Date() })
.where(and(eq(generations.id, id), eq(generations.userId, this.userId)));
};
const result = await (trx ? executeUpdate(trx) : this.db.transaction(executeUpdate));
log('Generation %s updated successfully', id);
return result;
}
async createAssetAndFile(
id: string,
asset: ImageGenerationAsset,
file: Omit<NewFile, 'id' | 'userId'>,
) {
log('Creating generation asset and file with transaction: %s', id);
return await this.db.transaction(async (tx: Transaction) => {
// Create file first using transaction
// Since duplicates are very rare, we always create globalFile - checking existence first would be wasteful
const newFile = await this.fileModel.create(
{
...file,
source: FileSource.ImageGeneration,
},
true,
tx,
);
// Update generation with asset and fileId using the transaction-aware update method
await this.update(
id,
{
asset,
fileId: newFile.id,
},
tx,
);
log('Generation %s updated with asset and file %s successfully', id, newFile.id);
return {
file: newFile,
};
});
}
async delete(id: string, trx?: Transaction) {
log('Deleting generation: %s for user: %s', id, this.userId);
const executeDelete = async (tx: Transaction) => {
return await tx
.delete(generations)
.where(and(eq(generations.id, id), eq(generations.userId, this.userId)))
.returning();
};
const result = await (trx ? executeDelete(trx) : this.db.transaction(executeDelete));
const deletedGeneration = result[0];
log('Generation %s deleted successfully', id);
return deletedGeneration;
}
/**
* Find generation by ID and transform it to frontend type
* This method uses findByIdWithAsyncTask and applies transformation
*/
async findByIdAndTransform(id: string): Promise<Generation | null> {
log('Finding and transforming generation: %s', id);
const generation = await this.findByIdWithAsyncTask(id);
if (!generation) {
log('Generation %s not found', id);
return null;
}
return await this.transformGeneration(generation);
}
/**
* Transform a GenerationItem (database type) to Generation (frontend type)
* This method processes asset URLs and async task information
*/
async transformGeneration(generation: GenerationWithAsyncTask): Promise<Generation> {
// Process asset URLs if they exist, following the same logic as in generationBatch.ts
const asset = generation.asset as ImageGenerationAsset | null;
if (asset && asset.url && asset.thumbnailUrl) {
const [url, thumbnailUrl] = await Promise.all([
this.fileService.getFullFileUrl(asset.url),
this.fileService.getFullFileUrl(asset.thumbnailUrl),
]);
asset.url = url;
asset.thumbnailUrl = thumbnailUrl;
}
// Build the Generation object following the same structure as in generationBatch.ts
const result: Generation = {
asset,
asyncTaskId: generation.asyncTaskId || null,
createdAt: generation.createdAt,
id: generation.id,
seed: generation.seed,
task: {
error: generation.asyncTask?.error
? (generation.asyncTask.error as AsyncTaskError)
: undefined,
id: generation.asyncTaskId || '',
status: (generation.asyncTask?.status as AsyncTaskStatus) || 'pending',
},
};
return result;
}
}