mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-30 15:59:09 +08:00
- Revised the README files for shared data and main data layers to improve clarity and structure. - Consolidated documentation on shared data types and API types, removing the now-deleted `api-design-guidelines.md`. - Streamlined directory structure descriptions and updated links to relevant documentation. - Enhanced quick reference sections for better usability and understanding of the data architecture.
8.5 KiB
8.5 KiB
DataApi in Main Process
This guide covers how to implement API handlers, services, and repositories in the Main process.
Architecture Layers
Handlers → Services → Repositories → Database
- Handlers: Thin layer, extract params, call service, transform response
- Services: Business logic, validation, transaction coordination
- Repositories: Data access (for complex domains)
- Database: Drizzle ORM + SQLite
Implementing Handlers
Location
src/main/data/api/handlers/
Handler Responsibilities
- Extract parameters from request
- Delegate to business service
- Transform response for IPC
- NO business logic here
Example Handler
// handlers/topic.ts
import type { ApiImplementation } from '@shared/data/api'
import { TopicService } from '@data/services/TopicService'
export const topicHandlers: Partial<ApiImplementation> = {
'/topics': {
GET: async ({ query }) => {
const { page = 1, limit = 20 } = query ?? {}
return await TopicService.getInstance().list({ page, limit })
},
POST: async ({ body }) => {
return await TopicService.getInstance().create(body)
}
},
'/topics/:id': {
GET: async ({ params }) => {
return await TopicService.getInstance().getById(params.id)
},
PUT: async ({ params, body }) => {
return await TopicService.getInstance().replace(params.id, body)
},
PATCH: async ({ params, body }) => {
return await TopicService.getInstance().update(params.id, body)
},
DELETE: async ({ params }) => {
await TopicService.getInstance().delete(params.id)
}
}
}
Register Handlers
// handlers/index.ts
import { topicHandlers } from './topic'
import { messageHandlers } from './message'
export const allHandlers: ApiImplementation = {
...topicHandlers,
...messageHandlers
}
Implementing Services
Location
src/main/data/services/
Service Responsibilities
- Business validation
- Transaction coordination
- Domain workflows
- Call repositories or direct Drizzle
Example Service
// services/TopicService.ts
import { DbService } from '@data/db/DbService'
import { TopicRepository } from '@data/repositories/TopicRepository'
import { DataApiErrorFactory } from '@shared/data/api'
export class TopicService {
private static instance: TopicService
private topicRepo: TopicRepository
private constructor() {
this.topicRepo = new TopicRepository()
}
static getInstance(): TopicService {
if (!this.instance) {
this.instance = new TopicService()
}
return this.instance
}
async list(options: { page: number; limit: number }) {
return await this.topicRepo.findAll(options)
}
async getById(id: string) {
const topic = await this.topicRepo.findById(id)
if (!topic) {
throw DataApiErrorFactory.notFound('Topic', id)
}
return topic
}
async create(data: CreateTopicDto) {
// Business validation
this.validateTopicData(data)
return await this.topicRepo.create(data)
}
async update(id: string, data: Partial<UpdateTopicDto>) {
const existing = await this.getById(id) // Throws if not found
return await this.topicRepo.update(id, data)
}
async delete(id: string) {
await this.getById(id) // Throws if not found
await this.topicRepo.delete(id)
}
private validateTopicData(data: CreateTopicDto) {
if (!data.name?.trim()) {
throw DataApiErrorFactory.validation({ name: ['Name is required'] })
}
}
}
Service with Transaction
async createTopicWithMessage(data: CreateTopicWithMessageDto) {
return await DbService.transaction(async (tx) => {
// Create topic
const topic = await this.topicRepo.create(data.topic, tx)
// Create initial message
const message = await this.messageRepo.create({
...data.message,
topicId: topic.id
}, tx)
return { topic, message }
})
}
Implementing Repositories
When to Use Repository Pattern
Use repositories for complex domains:
- ✅ Complex queries (joins, subqueries, aggregations)
- ✅ GB-scale data requiring pagination
- ✅ Complex transactions involving multiple tables
- ✅ Reusable data access patterns
- ✅ High testing requirements
When to Use Direct Drizzle
Use direct Drizzle for simple domains:
- ✅ Simple CRUD operations
- ✅ Small datasets (< 100MB)
- ✅ Domain-specific queries with no reuse
- ✅ Fast development is priority
Example Repository
// repositories/TopicRepository.ts
import { eq, desc, sql } from 'drizzle-orm'
import { DbService } from '@data/db/DbService'
import { topicTable } from '@data/db/schemas/topic'
export class TopicRepository {
async findAll(options: { page: number; limit: number }) {
const { page, limit } = options
const offset = (page - 1) * limit
const [items, countResult] = await Promise.all([
DbService.db
.select()
.from(topicTable)
.orderBy(desc(topicTable.updatedAt))
.limit(limit)
.offset(offset),
DbService.db
.select({ count: sql<number>`count(*)` })
.from(topicTable)
])
return {
items,
total: countResult[0].count,
page,
limit
}
}
async findById(id: string, tx?: Transaction) {
const db = tx || DbService.db
const [topic] = await db
.select()
.from(topicTable)
.where(eq(topicTable.id, id))
.limit(1)
return topic ?? null
}
async create(data: CreateTopicDto, tx?: Transaction) {
const db = tx || DbService.db
const [topic] = await db
.insert(topicTable)
.values(data)
.returning()
return topic
}
async update(id: string, data: Partial<UpdateTopicDto>, tx?: Transaction) {
const db = tx || DbService.db
const [topic] = await db
.update(topicTable)
.set(data)
.where(eq(topicTable.id, id))
.returning()
return topic
}
async delete(id: string, tx?: Transaction) {
const db = tx || DbService.db
await db
.delete(topicTable)
.where(eq(topicTable.id, id))
}
}
Example: Direct Drizzle in Service
For simple domains, skip the repository:
// services/TagService.ts
import { eq } from 'drizzle-orm'
import { DbService } from '@data/db/DbService'
import { tagTable } from '@data/db/schemas/tag'
export class TagService {
async getAll() {
return await DbService.db.select().from(tagTable)
}
async create(name: string) {
const [tag] = await DbService.db
.insert(tagTable)
.values({ name })
.returning()
return tag
}
async delete(id: string) {
await DbService.db
.delete(tagTable)
.where(eq(tagTable.id, id))
}
}
Error Handling
Using DataApiErrorFactory
import { DataApiErrorFactory } from '@shared/data/api'
// Not found
throw DataApiErrorFactory.notFound('Topic', id)
// Validation error
throw DataApiErrorFactory.validation({
name: ['Name is required', 'Name must be at least 3 characters'],
email: ['Invalid email format']
})
// Database error
try {
await db.insert(table).values(data)
} catch (error) {
throw DataApiErrorFactory.database(error, 'insert topic')
}
// Invalid operation
throw DataApiErrorFactory.invalidOperation(
'delete root message',
'cascade=true required'
)
// Conflict
throw DataApiErrorFactory.conflict('Topic name already exists')
// Timeout
throw DataApiErrorFactory.timeout('fetch topics', 3000)
Adding New Endpoints
Step-by-Step
- Define schema in
packages/shared/data/api/schemas/
// schemas/topic.ts
export interface TopicSchemas {
'/topics': {
GET: { response: PaginatedResponse<Topic> }
POST: { body: CreateTopicDto; response: Topic }
}
}
- Register schema in
schemas/index.ts
export type ApiSchemas = AssertValidSchemas<TopicSchemas & MessageSchemas>
-
Create service in
services/ -
Create repository (if complex) in
repositories/ -
Implement handler in
handlers/ -
Register handler in
handlers/index.ts
Best Practices
- Keep handlers thin: Only extract params and call services
- Put logic in services: All business rules belong in services
- Use repositories selectively: Simple CRUD doesn't need a repository
- Always use
.returning(): Get inserted/updated data without re-querying - Support transactions: Accept optional
txparameter in repositories - Validate in services: Business validation belongs in the service layer
- Use error factory: Consistent error creation with
DataApiErrorFactory