feat: Add example data seeding on startup
- Implement `ExampleDataService` to export/import Characters, Equipments, Poses, Expressions, and Generations. - Add `generate-example` script to `package.json` to generate example data from current DB. - Add startup logic in `src/index.ts` to seed example data if system is uninitialized and no data exists. - Store system initialization state in `Setting` table. - Add comprehensive tests for `ExampleDataService` in `src/service/example-data.test.ts` mocking filesystem and database operations.
This commit is contained in:
1 parent
790ecf27a0
commit
dcfb885a01
5 files changed
+709
-1
No files matched your search
+2
-1
@@ -7,7 +7,8 @@
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"test": "bun test",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"generate-example": "bun run src/scripts/generate-example-data.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.40.0",
|
||||
|
||||
@@ -14,6 +14,9 @@ import outfit from './api/outfit'
|
||||
import pose from './api/pose'
|
||||
import restore from './api/restore'
|
||||
import setting from './api/setting'
|
||||
import { prisma } from './db'
|
||||
import { exampleDataService } from './service/example-data'
|
||||
import { settingService } from './service/setting'
|
||||
import index from './ui/index.html'
|
||||
|
||||
// Ensure data directories exist
|
||||
@@ -62,6 +65,22 @@ if (process.env.NODE_ENV === 'production') {
|
||||
// Export type for RPC
|
||||
export type AppType = typeof route
|
||||
|
||||
// Check and seed example data
|
||||
const initialized = await settingService.get('system_initialized')
|
||||
if (!initialized) {
|
||||
try {
|
||||
const hasData = (await prisma.character.count()) > 0
|
||||
if (!hasData) {
|
||||
console.log('🌱 Seeding example data...')
|
||||
await exampleDataService.import()
|
||||
console.log('✅ Example data seeded')
|
||||
}
|
||||
await settingService.set('system_initialized', 'true')
|
||||
} catch (error) {
|
||||
console.error('Failed to seed example data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve the frontend
|
||||
const server = Bun.serve({
|
||||
port: 3000,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { exampleDataService } from '../service/example-data'
|
||||
import { prisma } from '../db'
|
||||
|
||||
async function main() {
|
||||
console.log('Starting example data generation...')
|
||||
try {
|
||||
await exampleDataService.export()
|
||||
console.log('Example data generation completed successfully.')
|
||||
} catch (error) {
|
||||
console.error('Error generating example data:', error)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,359 @@
|
||||
import { describe, it, expect, mock, beforeEach } from 'bun:test'
|
||||
|
||||
// 1. Define mocks before importing the module under test
|
||||
// Mock filesystem operations
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const mockExistsSync = mock((path: string) => false)
|
||||
const mockMkdirSync = mock(() => undefined)
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const mockReadFileSync = mock((path: string) => Buffer.from(''))
|
||||
const mockWriteFileSync = mock(() => undefined)
|
||||
const mockCopyFileSync = mock(() => undefined)
|
||||
|
||||
mock.module('node:fs', () => ({
|
||||
existsSync: mockExistsSync,
|
||||
mkdirSync: mockMkdirSync,
|
||||
readFileSync: mockReadFileSync,
|
||||
writeFileSync: mockWriteFileSync,
|
||||
copyFileSync: mockCopyFileSync,
|
||||
}))
|
||||
|
||||
// Mock prisma client
|
||||
const mockPrisma = {
|
||||
character: {
|
||||
// @ts-ignore
|
||||
findMany: mock(() => Promise.resolve([])),
|
||||
// @ts-ignore
|
||||
create: mock(() => Promise.resolve({})),
|
||||
// @ts-ignore
|
||||
findUnique: mock(() => Promise.resolve({})),
|
||||
},
|
||||
equipment: {
|
||||
// @ts-ignore
|
||||
findMany: mock(() => Promise.resolve([])),
|
||||
// @ts-ignore
|
||||
create: mock(() => Promise.resolve({})),
|
||||
},
|
||||
pose: {
|
||||
// @ts-ignore
|
||||
findMany: mock(() => Promise.resolve([])),
|
||||
// @ts-ignore
|
||||
create: mock(() => Promise.resolve({})),
|
||||
},
|
||||
expression: {
|
||||
// @ts-ignore
|
||||
findMany: mock(() => Promise.resolve([])),
|
||||
// @ts-ignore
|
||||
create: mock(() => Promise.resolve({})),
|
||||
},
|
||||
generation: {
|
||||
// @ts-ignore
|
||||
findMany: mock(() => Promise.resolve([])),
|
||||
// @ts-ignore
|
||||
create: mock(() => Promise.resolve({})),
|
||||
},
|
||||
// @ts-ignore
|
||||
asset: { create: mock(() => Promise.resolve({ id: 'mock-asset-id' })) },
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
$transaction: mock((cb: any) => cb(mockPrisma)),
|
||||
}
|
||||
|
||||
mock.module('../db', () => ({
|
||||
prisma: mockPrisma,
|
||||
}))
|
||||
|
||||
// Mock asset service
|
||||
const mockAssetService = {
|
||||
createAssetFromBuffer: mock(() => Promise.resolve({ id: 'new-asset-id', path: 'new-path.webp' })),
|
||||
}
|
||||
|
||||
mock.module('./asset', () => ({
|
||||
assetService: mockAssetService,
|
||||
}))
|
||||
|
||||
// 2. Import the service to test
|
||||
import { exampleDataService } from './example-data'
|
||||
|
||||
describe('ExampleDataService', () => {
|
||||
beforeEach(() => {
|
||||
// Reset mocks before each test
|
||||
mockExistsSync.mockClear()
|
||||
mockMkdirSync.mockClear()
|
||||
mockReadFileSync.mockClear()
|
||||
mockWriteFileSync.mockClear()
|
||||
mockCopyFileSync.mockClear()
|
||||
|
||||
mockPrisma.character.findMany.mockClear()
|
||||
mockPrisma.equipment.findMany.mockClear()
|
||||
mockPrisma.pose.findMany.mockClear()
|
||||
mockPrisma.expression.findMany.mockClear()
|
||||
mockPrisma.generation.findMany.mockClear()
|
||||
|
||||
mockPrisma.character.create.mockClear()
|
||||
mockPrisma.character.findUnique.mockClear()
|
||||
mockPrisma.equipment.create.mockClear()
|
||||
mockPrisma.pose.create.mockClear()
|
||||
mockPrisma.expression.create.mockClear()
|
||||
mockPrisma.generation.create.mockClear()
|
||||
|
||||
mockAssetService.createAssetFromBuffer.mockClear()
|
||||
})
|
||||
|
||||
describe('export', () => {
|
||||
it('should export all data types to JSON files', async () => {
|
||||
// Setup mock return values for findMany
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mockPrisma.character.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'c1',
|
||||
name: 'Char 1',
|
||||
description: 'Desc 1',
|
||||
image: { name: 'img1.png', type: 'image/png', path: 'img1.webp' },
|
||||
},
|
||||
] as any)
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mockPrisma.equipment.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'e1',
|
||||
name: 'Eq 1',
|
||||
description: 'Desc E1',
|
||||
category: 'Cat1',
|
||||
subCategory: null,
|
||||
image: { name: 'imgE1.png', type: 'image/png', path: 'imgE1.webp' },
|
||||
},
|
||||
] as any)
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mockPrisma.pose.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'p1',
|
||||
name: 'Pose 1',
|
||||
image: { name: 'imgP1.png', type: 'image/png', path: 'imgP1.webp' },
|
||||
},
|
||||
] as any)
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mockPrisma.expression.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'ex1',
|
||||
name: 'Expr 1',
|
||||
image: { name: 'imgEx1.png', type: 'image/png', path: 'imgEx1.webp' },
|
||||
},
|
||||
] as any)
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mockPrisma.generation.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'g1',
|
||||
characterId: 'c1',
|
||||
userPrompt: 'prompt',
|
||||
pose: 'pose',
|
||||
expression: 'expr',
|
||||
image: { name: 'imgG1.png', type: 'image/png', path: 'imgG1.webp' },
|
||||
equipments: [{ equipmentId: 'e1' }],
|
||||
},
|
||||
] as any)
|
||||
|
||||
// Mock existsSync to return true for source files so copyFileSync is called
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mockExistsSync.mockImplementation((path: any) => path.startsWith('data/files'))
|
||||
|
||||
await exampleDataService.export()
|
||||
|
||||
// Verify directories created
|
||||
expect(mockMkdirSync).toHaveBeenCalledTimes(2) // example-data and example-data/assets
|
||||
|
||||
// Verify data fetching
|
||||
expect(mockPrisma.character.findMany).toHaveBeenCalled()
|
||||
expect(mockPrisma.equipment.findMany).toHaveBeenCalled()
|
||||
expect(mockPrisma.pose.findMany).toHaveBeenCalled()
|
||||
expect(mockPrisma.expression.findMany).toHaveBeenCalled()
|
||||
expect(mockPrisma.generation.findMany).toHaveBeenCalled()
|
||||
|
||||
// Verify JSON writing
|
||||
expect(mockWriteFileSync).toHaveBeenCalledTimes(5)
|
||||
|
||||
// Check content of written files (can check calls arguments)
|
||||
const calls = mockWriteFileSync.mock.calls
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const writtenFiles = calls.map((c: any) => c[0])
|
||||
expect(writtenFiles).toContain('example-data/characters.json')
|
||||
expect(writtenFiles).toContain('example-data/equipments.json')
|
||||
expect(writtenFiles).toContain('example-data/poses.json')
|
||||
expect(writtenFiles).toContain('example-data/expressions.json')
|
||||
expect(writtenFiles).toContain('example-data/looks.json')
|
||||
|
||||
// Verify asset copying
|
||||
// 1 char + 1 equip + 1 pose + 1 expr + 1 gen = 5 assets
|
||||
expect(mockCopyFileSync).toHaveBeenCalledTimes(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('import', () => {
|
||||
it('should import data from JSON files', async () => {
|
||||
// Mock file existence to simulate data present
|
||||
mockExistsSync.mockImplementation(() => true)
|
||||
|
||||
// Mock reading JSON files
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mockReadFileSync.mockImplementation((path: any) => {
|
||||
if (path.includes('characters.json'))
|
||||
return JSON.stringify([
|
||||
{
|
||||
id: 'c1',
|
||||
name: 'Char 1',
|
||||
description: 'Desc 1',
|
||||
asset: { name: 'img1', type: 'image/png', path: 'img1.webp' },
|
||||
},
|
||||
])
|
||||
if (path.includes('equipments.json'))
|
||||
return JSON.stringify([
|
||||
{
|
||||
id: 'e1',
|
||||
name: 'Eq 1',
|
||||
description: 'Desc E1',
|
||||
category: 'Cat1',
|
||||
subCategory: null,
|
||||
asset: { name: 'imgE1', type: 'image/png', path: 'imgE1.webp' },
|
||||
},
|
||||
])
|
||||
if (path.includes('poses.json'))
|
||||
return JSON.stringify([
|
||||
{
|
||||
id: 'p1',
|
||||
name: 'Pose 1',
|
||||
asset: { name: 'imgP1', type: 'image/png', path: 'imgP1.webp' },
|
||||
},
|
||||
])
|
||||
if (path.includes('expressions.json'))
|
||||
return JSON.stringify([
|
||||
{
|
||||
id: 'ex1',
|
||||
name: 'Expr 1',
|
||||
asset: { name: 'imgEx1', type: 'image/png', path: 'imgEx1.webp' },
|
||||
},
|
||||
])
|
||||
if (path.includes('looks.json'))
|
||||
return JSON.stringify([
|
||||
{
|
||||
id: 'g1',
|
||||
characterId: 'c1',
|
||||
userPrompt: 'prompt',
|
||||
pose: 'pose',
|
||||
expression: 'expr',
|
||||
asset: { name: 'imgG1', type: 'image/png', path: 'imgG1.webp' },
|
||||
equipmentIds: ['e1'],
|
||||
},
|
||||
])
|
||||
// Mock reading asset file
|
||||
return Buffer.from('fake-image-data')
|
||||
})
|
||||
|
||||
// Mock Prisma finds needed for validation (e.g. check character exists for generation)
|
||||
// @ts-ignore
|
||||
mockPrisma.character.findUnique.mockResolvedValue({ id: 'c1' })
|
||||
|
||||
await exampleDataService.import()
|
||||
|
||||
// Verify asset creation (5 assets)
|
||||
expect(mockAssetService.createAssetFromBuffer).toHaveBeenCalledTimes(5)
|
||||
|
||||
// Verify DB creation
|
||||
expect(mockPrisma.character.create).toHaveBeenCalledTimes(1)
|
||||
expect(mockPrisma.character.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ id: 'c1', name: 'Char 1' }),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(mockPrisma.equipment.create).toHaveBeenCalledTimes(1)
|
||||
expect(mockPrisma.equipment.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ id: 'e1', name: 'Eq 1' }),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(mockPrisma.pose.create).toHaveBeenCalledTimes(1)
|
||||
expect(mockPrisma.pose.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ id: 'p1', name: 'Pose 1' }),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(mockPrisma.expression.create).toHaveBeenCalledTimes(1)
|
||||
expect(mockPrisma.expression.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ id: 'ex1', name: 'Expr 1' }),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(mockPrisma.generation.create).toHaveBeenCalledTimes(1)
|
||||
expect(mockPrisma.generation.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
id: 'g1',
|
||||
characterId: 'c1',
|
||||
equipments: {
|
||||
create: [{ equipment: { connect: { id: 'e1' } } }],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should skip import if example data folder does not exist', async () => {
|
||||
mockExistsSync.mockReturnValue(false)
|
||||
|
||||
await exampleDataService.import()
|
||||
|
||||
expect(mockReadFileSync).not.toHaveBeenCalled()
|
||||
expect(mockPrisma.character.create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should skip generation import if referenced character is missing', async () => {
|
||||
mockExistsSync.mockImplementation(() => true)
|
||||
|
||||
// Only return looks.json
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mockReadFileSync.mockImplementation((path: any) => {
|
||||
if (path.includes('looks.json'))
|
||||
return JSON.stringify([
|
||||
{
|
||||
id: 'g1',
|
||||
characterId: 'missing-char',
|
||||
userPrompt: 'prompt',
|
||||
pose: 'pose',
|
||||
expression: 'expr',
|
||||
asset: { name: 'imgG1', type: 'image/png', path: 'imgG1.webp' },
|
||||
equipmentIds: ['e1'],
|
||||
},
|
||||
])
|
||||
return Buffer.from('')
|
||||
})
|
||||
|
||||
// Return null for character lookup
|
||||
// @ts-ignore
|
||||
mockPrisma.character.findUnique.mockResolvedValue(null)
|
||||
|
||||
// Prevent reading other files to focus test
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mockExistsSync.mockImplementation((path: any) =>
|
||||
path.includes('looks.json') || path.includes('assets'),
|
||||
)
|
||||
|
||||
await exampleDataService.import()
|
||||
|
||||
expect(mockPrisma.generation.create).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,312 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { prisma } from '../db'
|
||||
import { assetService } from './asset'
|
||||
|
||||
interface AssetData {
|
||||
name: string
|
||||
type: string
|
||||
path: string // filename in example-data/assets
|
||||
}
|
||||
|
||||
interface CharacterData {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
asset: AssetData
|
||||
}
|
||||
|
||||
interface EquipmentData {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
subCategory: string | null
|
||||
asset: AssetData
|
||||
}
|
||||
|
||||
interface PoseData {
|
||||
id: string
|
||||
name: string
|
||||
asset: AssetData
|
||||
}
|
||||
|
||||
interface ExpressionData {
|
||||
id: string
|
||||
name: string
|
||||
asset: AssetData
|
||||
}
|
||||
|
||||
interface GenerationData {
|
||||
id: string
|
||||
characterId: string
|
||||
userPrompt: string | null
|
||||
pose: string | null
|
||||
expression: string | null
|
||||
asset: AssetData
|
||||
equipmentIds: string[]
|
||||
}
|
||||
|
||||
export class ExampleDataService {
|
||||
private baseDir = 'example-data'
|
||||
private assetsDir = join(this.baseDir, 'assets')
|
||||
|
||||
constructor() {
|
||||
// ensure base directories exist on instantiation (or method call)
|
||||
}
|
||||
|
||||
private ensureDirs() {
|
||||
if (!existsSync(this.baseDir)) {
|
||||
mkdirSync(this.baseDir)
|
||||
}
|
||||
if (!existsSync(this.assetsDir)) {
|
||||
mkdirSync(this.assetsDir)
|
||||
}
|
||||
}
|
||||
|
||||
async export() {
|
||||
this.ensureDirs()
|
||||
|
||||
// 1. Characters
|
||||
const characters = await prisma.character.findMany({ include: { image: true } })
|
||||
const characterData: CharacterData[] = characters.map((c) => {
|
||||
this.copyAsset(c.image.path)
|
||||
return {
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
asset: {
|
||||
name: c.image.name,
|
||||
type: c.image.type,
|
||||
path: c.image.path,
|
||||
},
|
||||
}
|
||||
})
|
||||
writeFileSync(join(this.baseDir, 'characters.json'), JSON.stringify(characterData, null, 2))
|
||||
|
||||
// 2. Equipments
|
||||
const equipments = await prisma.equipment.findMany({ include: { image: true } })
|
||||
const equipmentData: EquipmentData[] = equipments.map((e) => {
|
||||
this.copyAsset(e.image.path)
|
||||
return {
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
description: e.description,
|
||||
category: e.category,
|
||||
subCategory: e.subCategory,
|
||||
asset: {
|
||||
name: e.image.name,
|
||||
type: e.image.type,
|
||||
path: e.image.path,
|
||||
},
|
||||
}
|
||||
})
|
||||
writeFileSync(join(this.baseDir, 'equipments.json'), JSON.stringify(equipmentData, null, 2))
|
||||
|
||||
// 3. Poses
|
||||
const poses = await prisma.pose.findMany({ include: { image: true } })
|
||||
const poseData: PoseData[] = poses.map((p) => {
|
||||
this.copyAsset(p.image.path)
|
||||
return {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
asset: {
|
||||
name: p.image.name,
|
||||
type: p.image.type,
|
||||
path: p.image.path,
|
||||
},
|
||||
}
|
||||
})
|
||||
writeFileSync(join(this.baseDir, 'poses.json'), JSON.stringify(poseData, null, 2))
|
||||
|
||||
// 4. Expressions
|
||||
const expressions = await prisma.expression.findMany({ include: { image: true } })
|
||||
const expressionData: ExpressionData[] = expressions.map((e) => {
|
||||
this.copyAsset(e.image.path)
|
||||
return {
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
asset: {
|
||||
name: e.image.name,
|
||||
type: e.image.type,
|
||||
path: e.image.path,
|
||||
},
|
||||
}
|
||||
})
|
||||
writeFileSync(join(this.baseDir, 'expressions.json'), JSON.stringify(expressionData, null, 2))
|
||||
|
||||
// 5. Generations (Looks)
|
||||
const generations = await prisma.generation.findMany({
|
||||
include: {
|
||||
image: true,
|
||||
equipments: true,
|
||||
},
|
||||
})
|
||||
const generationData: GenerationData[] = generations.map((g) => {
|
||||
this.copyAsset(g.image.path)
|
||||
return {
|
||||
id: g.id,
|
||||
characterId: g.characterId,
|
||||
userPrompt: g.userPrompt,
|
||||
pose: g.pose,
|
||||
expression: g.expression,
|
||||
asset: {
|
||||
name: g.image.name,
|
||||
type: g.image.type,
|
||||
path: g.image.path,
|
||||
},
|
||||
equipmentIds: g.equipments.map((e) => e.equipmentId),
|
||||
}
|
||||
})
|
||||
writeFileSync(join(this.baseDir, 'looks.json'), JSON.stringify(generationData, null, 2))
|
||||
|
||||
console.log(`Exported ${characters.length} characters`)
|
||||
console.log(`Exported ${equipments.length} equipments`)
|
||||
console.log(`Exported ${poses.length} poses`)
|
||||
console.log(`Exported ${expressions.length} expressions`)
|
||||
console.log(`Exported ${generations.length} looks`)
|
||||
}
|
||||
|
||||
async import() {
|
||||
if (!existsSync(this.baseDir)) {
|
||||
console.log('No example data found, skipping import.')
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Characters
|
||||
if (existsSync(join(this.baseDir, 'characters.json'))) {
|
||||
const data: CharacterData[] = JSON.parse(
|
||||
readFileSync(join(this.baseDir, 'characters.json'), 'utf-8'),
|
||||
)
|
||||
for (const item of data) {
|
||||
const asset = await this.createAssetFromExample(item.asset)
|
||||
await prisma.character.create({
|
||||
data: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
imageId: asset.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(`Imported ${data.length} characters`)
|
||||
}
|
||||
|
||||
// 2. Equipments
|
||||
if (existsSync(join(this.baseDir, 'equipments.json'))) {
|
||||
const data: EquipmentData[] = JSON.parse(
|
||||
readFileSync(join(this.baseDir, 'equipments.json'), 'utf-8'),
|
||||
)
|
||||
for (const item of data) {
|
||||
const asset = await this.createAssetFromExample(item.asset)
|
||||
await prisma.equipment.create({
|
||||
data: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
category: item.category,
|
||||
subCategory: item.subCategory,
|
||||
imageId: asset.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(`Imported ${data.length} equipments`)
|
||||
}
|
||||
|
||||
// 3. Poses
|
||||
if (existsSync(join(this.baseDir, 'poses.json'))) {
|
||||
const data: PoseData[] = JSON.parse(
|
||||
readFileSync(join(this.baseDir, 'poses.json'), 'utf-8'),
|
||||
)
|
||||
for (const item of data) {
|
||||
const asset = await this.createAssetFromExample(item.asset)
|
||||
await prisma.pose.create({
|
||||
data: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
imageId: asset.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(`Imported ${data.length} poses`)
|
||||
}
|
||||
|
||||
// 4. Expressions
|
||||
if (existsSync(join(this.baseDir, 'expressions.json'))) {
|
||||
const data: ExpressionData[] = JSON.parse(
|
||||
readFileSync(join(this.baseDir, 'expressions.json'), 'utf-8'),
|
||||
)
|
||||
for (const item of data) {
|
||||
const asset = await this.createAssetFromExample(item.asset)
|
||||
await prisma.expression.create({
|
||||
data: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
imageId: asset.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(`Imported ${data.length} expressions`)
|
||||
}
|
||||
|
||||
// 5. Generations (Looks)
|
||||
if (existsSync(join(this.baseDir, 'looks.json'))) {
|
||||
const data: GenerationData[] = JSON.parse(
|
||||
readFileSync(join(this.baseDir, 'looks.json'), 'utf-8'),
|
||||
)
|
||||
for (const item of data) {
|
||||
// Ensure character exists
|
||||
const characterExists = await prisma.character.findUnique({
|
||||
where: { id: item.characterId },
|
||||
})
|
||||
if (!characterExists) {
|
||||
console.warn(`Skipping look ${item.id}: Character ${item.characterId} not found`)
|
||||
continue
|
||||
}
|
||||
|
||||
const asset = await this.createAssetFromExample(item.asset)
|
||||
await prisma.generation.create({
|
||||
data: {
|
||||
id: item.id,
|
||||
characterId: item.characterId,
|
||||
userPrompt: item.userPrompt,
|
||||
pose: item.pose,
|
||||
expression: item.expression,
|
||||
imageId: asset.id,
|
||||
equipments: {
|
||||
create: item.equipmentIds.map((id) => ({
|
||||
equipment: { connect: { id } },
|
||||
})),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(`Imported ${data.length} looks`)
|
||||
}
|
||||
}
|
||||
|
||||
private copyAsset(filename: string) {
|
||||
const src = join('data/files', filename)
|
||||
const dest = join(this.assetsDir, filename)
|
||||
if (existsSync(src)) {
|
||||
copyFileSync(src, dest)
|
||||
} else {
|
||||
console.warn(`Asset file not found: ${src}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async createAssetFromExample(assetData: AssetData) {
|
||||
const filePath = join(this.assetsDir, assetData.path)
|
||||
if (!existsSync(filePath)) {
|
||||
throw new Error(`Example asset file not found: ${filePath}`)
|
||||
}
|
||||
const buffer = readFileSync(filePath)
|
||||
// We use createAssetFromBuffer which handles saving to data/files and DB creation
|
||||
return assetService.createAssetFromBuffer(buffer, {
|
||||
name: assetData.name,
|
||||
type: assetData.type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const exampleDataService = new ExampleDataService()
|
||||
Reference in new issue
Block a user