Initial WallMuse project
This commit is contained in:
20
packages/db/package.json
Executable file
20
packages/db/package.json
Executable 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"
|
||||
}
|
||||
}
|
||||
457
packages/db/prisma/schema.prisma
Normal file
457
packages/db/prisma/schema.prisma
Normal 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
1
packages/db/src/index.ts
Executable file
@@ -0,0 +1 @@
|
||||
export * from "./json-store";
|
||||
523
packages/db/src/json-store.ts
Executable file
523
packages/db/src/json-store.ts
Executable 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
13
packages/db/tsconfig.json
Executable file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user