Initial WallMuse project

This commit is contained in:
fenglee
2026-05-09 09:12:41 +00:00
commit 3ea7d29827
91 changed files with 13136 additions and 0 deletions

20
packages/db/package.json Executable file
View File

@@ -0,0 +1,20 @@
{
"name": "@wallmuse/db",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"typecheck": "tsc --noEmit -p tsconfig.json",
"prisma": "prisma --schema prisma/schema.prisma",
"prisma:validate": "prisma validate --schema prisma/schema.prisma",
"prisma:generate": "prisma generate --schema prisma/schema.prisma"
},
"dependencies": {
"@prisma/client": "^6.7.0"
},
"devDependencies": {
"prisma": "^6.7.0"
}
}

View File

@@ -0,0 +1,457 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserRole {
user
admin
super_admin
}
enum UserStatus {
active
disabled
deleted
}
enum ProviderStatus {
disabled
healthy
degraded
error
}
enum ModelStatus {
draft
enabled
disabled
deprecated
}
enum ApiKeyMode {
platform
user_own
hybrid
}
enum AuthType {
bearer
api_key
custom
}
enum GenerationMode {
text_to_image
image_to_image
}
enum GenerationQuality {
standard
hd
ultra
}
enum ResolutionTier {
one_k @map("1k")
two_k @map("2k")
four_k @map("4k")
}
enum AspectRatio {
square @map("1:1")
landscape_4_3 @map("4:3")
portrait_3_4 @map("3:4")
landscape_16_9 @map("16:9")
portrait_9_16 @map("9:16")
ultrawide_21_9 @map("21:9")
}
enum GenerationGroupStatus {
queued
running
partial_succeeded
succeeded
failed
canceled
}
enum GenerationTaskStatus {
created
queued
dispatching
running
uploading
post_processing
moderating
succeeded
failed
retrying
canceled
expired
}
enum AssetKind {
reference
master
landscape
portrait
thumbnail
preview
download_zip
}
enum AssetStatus {
temporary
active
deleted
failed
}
enum WallpaperStatus {
draft
published
hidden
rejected
deleted
}
enum ModerationStatus {
pending
passed
rejected
manual_review
}
enum ProviderCallStatus {
started
success
failed
}
enum DevicePlatform {
web
ios
android
desktop
}
model User {
id String @id @default(uuid()) @db.Uuid
email String @unique @db.VarChar(255)
name String? @db.VarChar(80)
passwordHash String @map("password_hash") @db.VarChar(255)
roles UserRole[] @default([user])
status UserStatus @default(active)
userApiKeys UserApiKey[]
generationGroups GenerationGroup[]
wallpapers Wallpaper[]
preferences UserPreference?
devices Device[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users")
}
model UserApiKey {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
providerId String @map("provider_id") @db.Uuid
name String @db.VarChar(80)
maskedKey String @map("masked_key") @db.VarChar(120)
encryptedKey String @map("encrypted_key") @db.Text
baseUrl String? @map("base_url") @db.VarChar(512)
defaultModelId String? @map("default_model_id") @db.Uuid
enabled Boolean @default(true)
metadata Json @default("{}")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
provider Provider @relation(fields: [providerId], references: [id], onDelete: Restrict)
defaultModel Model? @relation(fields: [defaultModelId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@index([userId, enabled])
@@index([providerId])
@@map("user_api_keys")
}
model Provider {
id String @id @default(uuid()) @db.Uuid
slug String @unique @db.VarChar(80)
displayName String @map("display_name") @db.VarChar(120)
baseUrl String? @map("base_url") @db.VarChar(512)
authType AuthType @default(bearer) @map("auth_type")
status ProviderStatus @default(healthy)
keyMode ApiKeyMode @default(hybrid) @map("key_mode")
supportsUserKeys Boolean @default(true) @map("supports_user_keys")
supportsPlatformKeys Boolean @default(true) @map("supports_platform_keys")
healthCheckPath String? @map("health_check_path") @db.VarChar(255)
rateLimitPerMinute Int? @map("rate_limit_per_minute")
metadata Json @default("{}")
models Model[]
userApiKeys UserApiKey[]
generationGroups GenerationGroup[]
providerCallLogs ProviderCallLog[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("providers")
}
model Model {
id String @id @default(uuid()) @db.Uuid
providerId String @map("provider_id") @db.Uuid
slug String @db.VarChar(160)
displayName String @map("display_name") @db.VarChar(160)
status ModelStatus @default(enabled)
keyMode ApiKeyMode @default(hybrid) @map("key_mode")
capability Json
pricing Json?
defaultParams Json @default("{}") @map("default_params")
sortOrder Int @default(0) @map("sort_order")
provider Provider @relation(fields: [providerId], references: [id], onDelete: Cascade)
sizePresets ModelSizePreset[]
userApiKeys UserApiKey[]
generationGroups GenerationGroup[]
generationTasks GenerationTask[]
generatedAssets GeneratedAsset[]
providerCallLogs ProviderCallLog[]
wallpapers Wallpaper[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([providerId, slug])
@@index([status, sortOrder])
@@map("models")
}
model ModelSizePreset {
id String @id @default(uuid()) @db.Uuid
modelId String @map("model_id") @db.Uuid
aspectRatio AspectRatio @map("aspect_ratio")
resolution ResolutionTier
width Int
height Int
providerSizeValue String? @map("provider_size_value") @db.VarChar(80)
native Boolean @default(true)
requiresUpscale Boolean @default(false) @map("requires_upscale")
model Model @relation(fields: [modelId], references: [id], onDelete: Cascade)
@@unique([modelId, aspectRatio, resolution])
@@map("model_size_presets")
}
model GenerationGroup {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
providerId String @map("provider_id") @db.Uuid
modelId String @map("model_id") @db.Uuid
userApiKeyId String? @map("user_api_key_id") @db.Uuid
status GenerationGroupStatus @default(queued)
mode GenerationMode
prompt String @db.Text
negativePrompt String? @map("negative_prompt") @db.Text
resolution ResolutionTier
quality GenerationQuality @default(standard)
batchSize Int @default(1) @map("batch_size")
seed Int?
privacy String @default("private") @db.VarChar(24)
metadata Json @default("{}")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
provider Provider @relation(fields: [providerId], references: [id], onDelete: Restrict)
model Model @relation(fields: [modelId], references: [id], onDelete: Restrict)
tasks GenerationTask[]
assets GeneratedAsset[]
wallpapers Wallpaper[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([userId, createdAt])
@@index([status, createdAt])
@@map("generation_groups")
}
model GenerationTask {
id String @id @default(uuid()) @db.Uuid
groupId String @map("group_id") @db.Uuid
modelId String @map("model_id") @db.Uuid
status GenerationTaskStatus @default(created)
mode GenerationMode
aspectRatio AspectRatio @map("aspect_ratio")
resolution ResolutionTier
quality GenerationQuality @default(standard)
priority Int @default(0)
attempt Int @default(0)
maxAttempts Int @default(3) @map("max_attempts")
progress Int @default(0)
errorCode String? @map("error_code") @db.VarChar(120)
errorMessage String? @map("error_message") @db.Text
lockedAt DateTime? @map("locked_at")
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
group GenerationGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
model Model @relation(fields: [modelId], references: [id], onDelete: Restrict)
assets GeneratedAsset[]
providerCallLogs ProviderCallLog[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([status, priority, createdAt], map: "idx_generation_tasks_status_priority")
@@index([groupId])
@@map("generation_tasks")
}
model GeneratedAsset {
id String @id @default(uuid()) @db.Uuid
groupId String @map("group_id") @db.Uuid
taskId String? @map("task_id") @db.Uuid
providerId String? @map("provider_id") @db.Uuid
modelId String? @map("model_id") @db.Uuid
kind AssetKind
status AssetStatus @default(temporary)
width Int?
height Int?
mimeType String? @map("mime_type") @db.VarChar(120)
bucket String? @db.VarChar(120)
objectKey String? @map("object_key") @db.VarChar(512)
storageUrl String? @map("storage_url") @db.Text
publicUrl String? @map("public_url") @db.Text
blurHash String? @map("blur_hash") @db.VarChar(255)
byteSize BigInt? @map("byte_size")
sha256 String? @db.VarChar(64)
metadata Json @default("{}")
group GenerationGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
task GenerationTask? @relation(fields: [taskId], references: [id], onDelete: SetNull)
model Model? @relation(fields: [modelId], references: [id], onDelete: SetNull)
wallpapers Wallpaper[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([groupId])
@@index([taskId])
@@map("generated_assets")
}
model Wallpaper {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
groupId String? @map("group_id") @db.Uuid
assetId String? @map("asset_id") @db.Uuid
modelId String? @map("model_id") @db.Uuid
title String @db.VarChar(160)
description String? @db.Text
prompt String? @db.Text
status WallpaperStatus @default(draft)
isFeatured Boolean @default(false) @map("is_featured")
tags String[] @default([])
metadata Json @default("{}")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
group GenerationGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)
asset GeneratedAsset? @relation(fields: [assetId], references: [id], onDelete: SetNull)
model Model? @relation(fields: [modelId], references: [id], onDelete: SetNull)
moderationRecords ModerationRecord[]
publishedAt DateTime? @map("published_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([status, isFeatured, publishedAt], map: "idx_wallpapers_status_featured_published")
@@index([userId, createdAt])
@@map("wallpapers")
}
model ModerationRecord {
id String @id @default(uuid()) @db.Uuid
wallpaperId String? @map("wallpaper_id") @db.Uuid
assetId String? @map("asset_id") @db.Uuid
status ModerationStatus @default(pending)
reason String? @db.Text
metadata Json @default("{}")
reviewerId String? @map("reviewer_id") @db.Uuid
wallpaper Wallpaper? @relation(fields: [wallpaperId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([status, createdAt])
@@map("moderation_records")
}
model ProviderCallLog {
id String @id @default(uuid()) @db.Uuid
taskId String? @map("task_id") @db.Uuid
providerId String @map("provider_id") @db.Uuid
modelId String? @map("model_id") @db.Uuid
userApiKeyId String? @map("user_api_key_id") @db.Uuid
status ProviderCallStatus
latencyMs Int? @map("latency_ms")
errorCode String? @map("error_code") @db.VarChar(120)
errorMessage String? @map("error_message") @db.Text
requestId String? @map("request_id") @db.VarChar(160)
providerCost Decimal? @map("provider_cost") @db.Decimal(12, 6)
metadata Json @default("{}")
task GenerationTask? @relation(fields: [taskId], references: [id], onDelete: SetNull)
provider Provider @relation(fields: [providerId], references: [id], onDelete: Restrict)
model Model? @relation(fields: [modelId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now()) @map("created_at")
@@index([taskId, createdAt], map: "idx_provider_logs_task")
@@index([providerId, createdAt])
@@map("provider_call_logs")
}
model UserPreference {
id String @id @default(uuid()) @db.Uuid
userId String @unique @map("user_id") @db.Uuid
defaultModelId String? @map("default_model_id") @db.Uuid
defaultResolution ResolutionTier? @map("default_resolution")
defaultAspectRatios String[] @default([]) @map("default_aspect_ratios")
defaultPrivacy String @default("private") @map("default_privacy") @db.VarChar(24)
metadata Json @default("{}")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("user_preferences")
}
model Device {
id String @id @default(uuid()) @db.Uuid
userId String? @map("user_id") @db.Uuid
platform DevicePlatform
appVersion String? @map("app_version") @db.VarChar(40)
pushToken String? @map("push_token") @db.Text
metadata Json @default("{}")
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([userId, platform])
@@map("devices")
}
model Notification {
id String @id @default(uuid()) @db.Uuid
userId String? @map("user_id") @db.Uuid
title String @db.VarChar(160)
body String @db.Text
readAt DateTime? @map("read_at")
metadata Json @default("{}")
createdAt DateTime @default(now()) @map("created_at")
@@index([userId, readAt, createdAt])
@@map("notifications")
}
model AppConfig {
key String @id @db.VarChar(120)
value Json
description String? @db.Text
updatedAt DateTime @updatedAt @map("updated_at")
@@map("app_config")
}

1
packages/db/src/index.ts Executable file
View File

@@ -0,0 +1 @@
export * from "./json-store";

523
packages/db/src/json-store.ts Executable file
View File

@@ -0,0 +1,523 @@
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import type {
AppConfigResponse,
AssetKind,
GeneratedAsset,
GenerationGroup,
GenerationGroupStatus,
GenerationTaskStatus,
ModelSummary,
ProviderSummary
} from "@wallmuse/shared";
type JsonUserRole = "user" | "admin" | "super_admin";
export interface UserRecord {
id: string;
email: string;
name?: string;
roles: JsonUserRole[];
passwordHash: string;
createdAt: string;
}
export interface UserApiKeyRecord {
id: string;
userId: string;
providerId: string;
name: string;
maskedKey: string;
encryptedKey: string;
baseUrl?: string;
defaultModelId?: string;
enabled: boolean;
createdAt: string;
updatedAt: string;
}
export interface ProviderRecord extends ProviderSummary {
baseUrl?: string;
authType: "bearer" | "api_key" | "custom";
supportsUserKeys: boolean;
supportsPlatformKeys: boolean;
healthCheckPath?: string;
createdAt: string;
updatedAt: string;
}
export interface ProviderCallLogRecord {
id: string;
taskId?: string;
groupId?: string;
providerId: string;
modelId?: string;
assetKind?: AssetKind;
status: "started" | "succeeded" | "success" | "failed";
attempt?: number;
latencyMs?: number;
errorCode?: string;
errorMessage?: string;
usage?: unknown;
rawMetadata?: unknown;
createdAt: string;
updatedAt?: string;
}
export interface GenerationWorkerJobData {
groupId: string;
taskId?: string;
userId: string;
mode: "text_to_image" | "image_to_image";
prompt: string;
negativePrompt?: string;
resolution: "1k" | "2k" | "4k";
providerId: string;
providerSlug: string;
modelId: string;
modelSlug: string;
referenceAssetUrl?: string;
retryAssetKind?: Extract<AssetKind, "master" | "landscape" | "portrait">;
seed?: number;
}
export interface StoredProviderAssetInput {
groupId: string;
taskId?: string;
userId: string;
assetKind: Extract<AssetKind, "master" | "landscape" | "portrait">;
status: "active" | "failed";
width?: number;
height?: number;
mimeType?: string;
publicUrl?: string;
storageUrl?: string;
objectKey?: string;
bucket?: string;
byteSize?: number;
sha256?: string;
providerId: string;
modelId: string;
seed?: number;
}
export interface StoredGenerationGroup extends GenerationGroup {
userId: string;
providerId: string;
privacy: "private" | "public";
}
export interface DataStoreShape {
appConfigUpdatedAt: string;
users: UserRecord[];
providers: ProviderRecord[];
models: ModelSummary[];
userApiKeys: UserApiKeyRecord[];
generationGroups: StoredGenerationGroup[];
providerCallLogs: ProviderCallLogRecord[];
}
const nowIso = (): string => new Date().toISOString();
const mockProviderId = "00000000-0000-4000-8000-000000000001";
const mockModelId = "00000000-0000-4000-8000-000000000101";
const createDefaultProvider = (now: string): ProviderRecord => ({
id: mockProviderId,
slug: "mock",
displayName: "Mock Provider",
status: "healthy",
keyMode: "hybrid",
modelCount: 1,
authType: "bearer",
supportsUserKeys: true,
supportsPlatformKeys: true,
healthCheckPath: "/v1/models",
createdAt: now,
updatedAt: now
});
const createDefaultModel = (): ModelSummary => ({
id: mockModelId,
providerId: mockProviderId,
slug: "mock-wallpaper-v1",
displayName: "Mock Wallpaper v1",
status: "enabled",
keyMode: "hybrid",
sortOrder: 0,
capability: {
supportsTextToImage: true,
supportsImageToImage: true,
supportsEdit: false,
supportsNegativePrompt: true,
supportsSeed: true,
supportsBatch: false,
supportsStreaming: false,
supportsBase64Result: false,
supportsUrlResult: true,
supportsNative4k: false,
maxBatchSize: 1,
maxInputImages: 3,
maxPromptLength: 4000,
maxNegativePromptLength: 2000,
maxPixels: 3686400,
supportedAspectRatios: ["16:9", "9:16", "1:1"],
supportedResolutions: ["1k", "2k", "4k"],
sizePresets: [
{
aspectRatio: "16:9",
resolution: "2k",
width: 2560,
height: 1440,
providerSizeValue: "2560x1440",
native: true,
requiresUpscale: false
},
{
aspectRatio: "9:16",
resolution: "2k",
width: 1440,
height: 2560,
providerSizeValue: "1440x2560",
native: true,
requiresUpscale: false
}
],
defaultParams: {}
},
pricing: {
currency: "USD",
unit: "image",
amount: 0,
estimatedCredits: 1
}
});
const emptyData = (): DataStoreShape => ({
appConfigUpdatedAt: nowIso(),
users: [],
providers: [],
models: [],
userApiKeys: [],
generationGroups: [],
providerCallLogs: []
});
export class JsonWallMuseDb {
private writeQueue = Promise.resolve();
constructor(private readonly filePath: string) {}
static fromEnv(): JsonWallMuseDb {
const dataDir = process.env.WALLMUSE_DATA_DIR ?? join(process.cwd(), ".data");
return new JsonWallMuseDb(join(dataDir, "wallmuse-api.json"));
}
async init(): Promise<void> {
await mkdir(dirname(this.filePath), { recursive: true });
const data = await this.read();
if (!data.providers.some((provider) => provider.slug === "mock")) {
const now = nowIso();
data.providers.push(createDefaultProvider(now));
data.models.push(createDefaultModel());
await this.write(data);
}
}
async read(): Promise<DataStoreShape> {
try {
const text = await readFile(this.filePath, "utf8");
const parsed = JSON.parse(text) as Partial<DataStoreShape>;
return { ...emptyData(), ...parsed };
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return emptyData();
}
throw error;
}
}
async mutate<T>(fn: (data: DataStoreShape) => T | Promise<T>): Promise<T> {
const run = async () => {
const data = await this.read();
const result = await fn(data);
await this.write(data);
return result;
};
const next = this.writeQueue.then(run, run);
this.writeQueue = next.then(
() => undefined,
() => undefined
);
return next;
}
getAppConfig(data: DataStoreShape): AppConfigResponse {
return {
site: { name: "WallMuse" },
generation: {
defaultModelId: data.models[0]?.id,
defaultAspectRatios: ["16:9", "9:16"],
defaultResolution: "2k",
maxBatchSize: 4,
allowedResolutions: ["1k", "2k", "4k"]
},
features: {
authEnabled: true,
galleryEnabled: true,
userApiKeysEnabled: true,
generationEnabled: true,
darkModeEnabled: true
},
providers: data.providers,
models: data.models,
updatedAt: data.appConfigUpdatedAt
};
}
async seedQueuedGeneration(job: GenerationWorkerJobData): Promise<void> {
await this.init();
const now = nowIso();
await this.mutate((data) => {
let group = data.generationGroups.find((item) => item.id === job.groupId);
if (!group) {
const taskId = job.taskId ?? crypto.randomUUID();
group = {
id: job.groupId,
userId: job.userId,
providerId: job.providerId,
privacy: "private",
status: "queued",
modelId: job.modelId,
prompt: job.prompt,
negativePrompt: job.negativePrompt,
tasks: [
{
id: taskId,
groupId: job.groupId,
status: "queued",
mode: job.mode,
aspectRatio: "16:9",
resolution: job.resolution,
quality: "standard",
attempt: 0,
maxAttempts: 3,
progress: 0,
createdAt: now,
updatedAt: now
},
{
id: crypto.randomUUID(),
groupId: job.groupId,
status: "queued",
mode: job.mode,
aspectRatio: "9:16",
resolution: job.resolution,
quality: "standard",
attempt: 0,
maxAttempts: 3,
progress: 0,
createdAt: now,
updatedAt: now
}
],
assets: makeGenerationAssets({ "16:9": taskId, "9:16": undefined }, now),
createdAt: now,
updatedAt: now
};
data.generationGroups.push(group);
}
const task = findWorkerTask(group, job.taskId);
if (task) {
task.status = "queued";
task.progress = 0;
task.errorCode = undefined;
task.errorMessage = undefined;
task.updatedAt = now;
}
group.status = "queued";
group.updatedAt = now;
});
}
async markGenerationStatus(
groupId: string,
taskId: string | undefined,
groupStatus: GenerationGroupStatus,
taskStatus: GenerationTaskStatus,
progress: number
): Promise<void> {
const now = nowIso();
await this.mutate((data) => {
const group = requireWorkerGenerationGroup(data, groupId);
const task = findWorkerTask(group, taskId);
group.status = groupStatus;
group.updatedAt = now;
if (task) {
task.status = taskStatus;
task.progress = progress;
task.updatedAt = now;
}
});
}
async incrementGenerationAttempt(groupId: string, taskId?: string): Promise<number> {
let attempt = 1;
await this.mutate((data) => {
const group = requireWorkerGenerationGroup(data, groupId);
const task = findWorkerTask(group, taskId);
if (task) {
task.attempt += 1;
task.updatedAt = nowIso();
attempt = task.attempt;
}
});
return attempt;
}
async markGenerationFailure(
groupId: string,
taskId: string | undefined,
groupStatus: "failed" | "partial_succeeded",
errorCode: string,
errorMessage: string
): Promise<void> {
const now = nowIso();
await this.mutate((data) => {
const group = requireWorkerGenerationGroup(data, groupId);
const task = findWorkerTask(group, taskId);
group.status = groupStatus;
group.updatedAt = now;
if (task) {
task.status = "failed";
task.errorCode = errorCode;
task.errorMessage = errorMessage;
task.updatedAt = now;
}
});
}
async createProviderCallLog(input: Omit<ProviderCallLogRecord, "createdAt" | "updatedAt" | "status">): Promise<ProviderCallLogRecord> {
const now = nowIso();
const log: ProviderCallLogRecord = { ...input, status: "started", createdAt: now, updatedAt: now };
await this.mutate((data) => {
data.providerCallLogs.push(log);
});
return log;
}
async completeProviderCallLog(
id: string,
patch: Pick<ProviderCallLogRecord, "status" | "latencyMs" | "usage" | "rawMetadata" | "errorCode" | "errorMessage">
): Promise<void> {
await this.mutate((data) => {
const log = data.providerCallLogs.find((item) => item.id === id);
if (!log) {
throw new Error(`Provider call log not found: ${id}`);
}
Object.assign(log, patch, { updatedAt: nowIso() });
});
}
async upsertGeneratedAsset(input: StoredProviderAssetInput): Promise<GeneratedAsset> {
const now = nowIso();
let asset: GeneratedAsset | undefined;
await this.mutate((data) => {
const group = requireWorkerGenerationGroup(data, input.groupId);
const existing = group.assets.find((item) => item.kind === input.assetKind);
const metadata = {
storageUrl: input.storageUrl,
objectKey: input.objectKey,
bucket: input.bucket,
byteSize: input.byteSize,
sha256: input.sha256,
providerId: input.providerId,
modelId: input.modelId,
seed: input.seed
};
if (existing) {
if (input.taskId !== undefined) existing.taskId = input.taskId;
existing.status = input.status;
existing.width = input.width;
existing.height = input.height;
existing.mimeType = input.mimeType;
existing.publicUrl = input.publicUrl;
Object.assign(existing as GeneratedAsset & { metadata?: unknown }, { metadata });
asset = existing;
return;
}
asset = {
id: crypto.randomUUID(),
...(input.taskId === undefined ? {} : { taskId: input.taskId }),
kind: input.assetKind,
status: input.status,
...(input.width === undefined ? {} : { width: input.width }),
...(input.height === undefined ? {} : { height: input.height }),
...(input.mimeType === undefined ? {} : { mimeType: input.mimeType }),
...(input.publicUrl === undefined ? {} : { publicUrl: input.publicUrl }),
createdAt: now
};
Object.assign(asset as GeneratedAsset & { metadata?: unknown }, { metadata });
group.assets.push(asset);
group.updatedAt = now;
});
return asset!;
}
private async write(data: DataStoreShape): Promise<void> {
await mkdir(dirname(this.filePath), { recursive: true });
const tmpPath = `${this.filePath}.${process.pid}.tmp`;
await writeFile(tmpPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
await rename(tmpPath, this.filePath);
}
}
export const toPublicUser = (user: UserRecord) => ({
id: user.id,
email: user.email,
name: user.name,
roles: user.roles,
createdAt: user.createdAt
});
export const hasAdminRole = (roles: JsonUserRole[]): boolean =>
roles.includes("admin") || roles.includes("super_admin");
export const makeGenerationAssets = (
taskIds: Record<"16:9" | "9:16", string | undefined>,
now: string
): GeneratedAsset[] => {
const assets: GeneratedAsset[] = [{ id: crypto.randomUUID(), kind: "master", status: "temporary", createdAt: now }];
if (taskIds["16:9"]) {
assets.push({
id: crypto.randomUUID(),
taskId: taskIds["16:9"],
kind: "landscape",
status: "temporary",
width: 2560,
height: 1440,
createdAt: now
});
}
if (taskIds["9:16"]) {
assets.push({
id: crypto.randomUUID(),
taskId: taskIds["9:16"],
kind: "portrait",
status: "temporary",
width: 1440,
height: 2560,
createdAt: now
});
}
return assets;
};
const requireWorkerGenerationGroup = (data: DataStoreShape, groupId: string): StoredGenerationGroup => {
const group = data.generationGroups.find((item) => item.id === groupId);
if (!group) throw new Error(`Generation group not found: ${groupId}`);
return group;
};
const findWorkerTask = (group: StoredGenerationGroup, taskId?: string) => group.tasks.find((task) => task.id === taskId) ?? group.tasks[0];

13
packages/db/tsconfig.json Executable file
View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"declaration": true,
"types": [
"node"
]
},
"include": [
"src/**/*.ts"
]
}