repository-pattern

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Repository Pattern

仓库模式

Overview

概述

This skill helps implement the repository pattern used in this workout tracker application. The pattern provides a clean abstraction over Dexie (IndexedDB) with type-safe interfaces, consistent error handling, and standardized CRUD operations.
本技能可帮助实现此健身追踪应用中使用的仓库模式。该模式通过类型安全的接口、一致的错误处理和标准化CRUD操作,为Dexie(IndexedDB)提供清晰的抽象层。

Architecture Overview

架构概述

Layered Approach:
Interfaces → Implementations → Provider → Public API
    ↓              ↓              ↓           ↓
db/interfaces  db/impl/dexie  db/provider  db/index
Flow:
  1. Define repository interface in
    db/interfaces.ts
  2. Implement with Dexie in
    db/implementations/dexie/[entity].ts
  3. Register in factory provider (
    db/implementations/dexie/index.ts
    )
  4. Export public getter (
    db/index.ts
    )
  5. Use in features via
    getEntityRepository()
分层架构:
Interfaces → Implementations → Provider → Public API
    ↓              ↓              ↓           ↓
db/interfaces  db/impl/dexie  db/provider  db/index
流程:
  1. db/interfaces.ts
    中定义仓库接口
  2. db/implementations/dexie/[entity].ts
    中使用Dexie实现接口
  3. 在工厂提供者(
    db/implementations/dexie/index.ts
    )中注册实现
  4. 导出公共获取器(
    db/index.ts
  5. 通过
    getEntityRepository()
    在功能模块中使用

Core Workflow

核心工作流

Follow these 6 steps when creating a new repository:
创建新仓库时请遵循以下6个步骤:

Step 1: Define Interface

步骤1:定义接口

Location:
src/db/interfaces.ts
Define the repository interface with standard CRUD methods:
typescript
export type EntityRepository = {
  getAll(): Promise<ReadonlyArray<DbEntity>>
  getById(id: string): Promise<DbEntity | undefined>
  create(entity: Omit<DbEntity, 'id' | 'createdAt'>): Promise<DbEntity>
  update(id: string, updates: Partial<Omit<DbEntity, 'id' | 'createdAt'>>): Promise<void>
  delete(id: string): Promise<void>
  // ... entity-specific queries
}
Add to
RepositoryProvider
:
typescript
export type RepositoryProvider = {
  // ... existing
  entity: EntityRepository
}
Guidelines:
  • Use
    ReadonlyArray<T>
    and
    Readonly<T>
    for arguments
  • getById
    returns
    undefined
    (not throw) when not found
  • create
    returns the created entity with generated ID
  • update
    and
    delete
    return
    void
  • Use
    Omit<>
    to exclude auto-generated fields (
    id
    ,
    createdAt
    )
位置:
src/db/interfaces.ts
使用标准CRUD方法定义仓库接口:
typescript
export type EntityRepository = {
  getAll(): Promise<ReadonlyArray<DbEntity>>
  getById(id: string): Promise<DbEntity | undefined>
  create(entity: Omit<DbEntity, 'id' | 'createdAt'>): Promise<DbEntity>
  update(id: string, updates: Partial<Omit<DbEntity, 'id' | 'createdAt'>>): Promise<void>
  delete(id: string): Promise<void>
  // ... entity-specific queries
}
添加至
RepositoryProvider
typescript
export type RepositoryProvider = {
  // ... existing
  entity: EntityRepository
}
指导原则:
  • 对参数使用
    ReadonlyArray<T>
    Readonly<T>
  • 当未找到实体时,
    getById
    返回
    undefined
    (而非抛出错误)
  • create
    返回带有生成ID的已创建实体
  • update
    delete
    返回
    void
  • 使用
    Omit<>
    排除自动生成的字段(
    id
    createdAt

Step 2: Add Schema Type

步骤2:添加模式类型

Location:
src/db/schema.ts
Define database type with
Db
prefix:
typescript
/**
 * Entity stored in database.
 * Uses null instead of undefined for explicit "no value" semantics.
 */
export type DbEntity = {
  id: string
  name: string
  value: string | null        // Use null, not undefined
  createdAt: number
  updatedAt: number | null    // null until first update
}
Key Conventions:
  • Always use
    Db
    prefix for database types
  • Use
    null
    for "no value" (not
    undefined
    )
  • Store user input numbers as
    string
    (e.g.,
    kg: string
    ,
    reps: string
    )
  • Use discriminated unions with
    kind
    property for variants
  • Include type guards if needed:
    export function isDbEntity(x: unknown): x is DbEntity
位置:
src/db/schema.ts
使用
Db
前缀定义数据库类型:
typescript
/**
 * Entity stored in database.
 * Uses null instead of undefined for explicit "no value" semantics.
 */
export type DbEntity = {
  id: string
  name: string
  value: string | null        // Use null, not undefined
  createdAt: number
  updatedAt: number | null    // null until first update
}
关键约定:
  • 数据库类型必须使用
    Db
    前缀
  • 使用
    null
    表示“无值”(而非
    undefined
  • 将用户输入的数字存储为
    string
    (例如:
    kg: string
    reps: string
  • 使用带有
    kind
    属性的可区分联合处理变体
  • 必要时包含类型守卫:
    export function isDbEntity(x: unknown): x is DbEntity

Step 3: Update Database Class

步骤3:更新数据库类

Location:
src/db/implementations/dexie/database.ts
Add table and indexes:
typescript
export class WorkoutTrackerDb extends Dexie {
  // ... existing tables
  entities!: Table<DbEntity, string>

  constructor() {
    super('WorkoutTracker')

    // Increment version number
    this.version(3).stores({
      // ... existing tables
      entities: 'id, name, createdAt',  // Index: primary + frequently queried fields
    })
  }
}
Indexing Guidelines:
  • Always index primary key (automatic)
  • Index fields used in
    where()
    ,
    orderBy()
    ,
    equals()
  • Index foreign keys for joins
  • Compound indexes for junction tables:
    '[field1+field2], field1, field2'
位置:
src/db/implementations/dexie/database.ts
添加表和索引:
typescript
export class WorkoutTrackerDb extends Dexie {
  // ... existing tables
  entities!: Table<DbEntity, string>

  constructor() {
    super('WorkoutTracker')

    // Increment version number
    this.version(3).stores({
      // ... existing tables
      entities: 'id, name, createdAt',  // Index: primary + frequently queried fields
    })
  }
}
索引指导原则:
  • 始终为主键创建索引(自动生成)
  • where()
    orderBy()
    equals()
    中使用的字段创建索引
  • 为外键创建索引以支持关联查询
  • 为关联表创建复合索引:
    '[field1+field2], field1, field2'

Step 4: Implement Repository

步骤4:实现仓库

Location:
src/db/implementations/dexie/[entity].ts
Create factory function returning repository implementation:
typescript
import type { EntityRepository } from '@/db/interfaces'
import type { DbEntity } from '@/db/schema'
import { createDatabaseError, tryCatch } from '@/lib/tryCatch'
import type { WorkoutTrackerDb } from './database'
import { generateId } from './database'

/**
 * Dexie implementation of EntityRepository.
 */
export function createDexieEntityRepository(db: WorkoutTrackerDb): EntityRepository {
  return {
    async getAll(): Promise<ReadonlyArray<DbEntity>> {
      const [error, entities] = await tryCatch(
        db.entities.orderBy('createdAt').reverse().toArray(),
      )
      if (error) {
        throw createDatabaseError('LOAD_FAILED', 'retrieve entities', error)
      }
      return entities
    },

    async getById(id: string): Promise<DbEntity | undefined> {
      const [error, entity] = await tryCatch(db.entities.get(id))
      if (error) {
        throw createDatabaseError('LOAD_FAILED', `retrieve entity with id ${id}`, error)
      }
      return entity
    },

    async create(
      entity: Omit<DbEntity, 'id' | 'createdAt'>,
    ): Promise<DbEntity> {
      const newEntity: DbEntity = {
        ...entity,
        id: generateId(),
        createdAt: Date.now(),
      }

      const [error] = await tryCatch(db.entities.add(newEntity))
      if (error) {
        throw createDatabaseError('SAVE_FAILED', 'create entity', error)
      }

      return newEntity
    },

    async update(
      id: string,
      updates: Partial<Omit<DbEntity, 'id' | 'createdAt'>>,
    ): Promise<void> {
      const [error, updatedCount] = await tryCatch(
        db.entities.update(id, {
          ...updates,
          updatedAt: Date.now(),  // Auto-inject timestamp
        }),
      )

      if (error) {
        throw createDatabaseError('SAVE_FAILED', `update entity with id ${id}`, error)
      }

      if (updatedCount === 0) {
        throw createDatabaseError('NOT_FOUND', `entity with id ${id} not found`)
      }
    },

    async delete(id: string): Promise<void> {
      const [error] = await tryCatch(db.entities.delete(id))
      if (error) {
        throw createDatabaseError('SAVE_FAILED', `delete entity with id ${id}`, error)
      }
      // Soft delete: no NOT_FOUND check
    },
  }
}
Key Patterns:
  • Use
    tryCatch()
    wrapper for all operations (preferred pattern)
  • Two-phase error checking: operation failure + not found
  • Auto-inject timestamps:
    createdAt
    ,
    updatedAt
  • Use
    generateId()
    for new IDs
  • Soft delete: no error if entity doesn't exist
位置:
src/db/implementations/dexie/[entity].ts
创建返回仓库实现的工厂函数:
typescript
import type { EntityRepository } from '@/db/interfaces'
import type { DbEntity } from '@/db/schema'
import { createDatabaseError, tryCatch } from '@/lib/tryCatch'
import type { WorkoutTrackerDb } from './database'
import { generateId } from './database'

/**
 * Dexie implementation of EntityRepository.
 */
export function createDexieEntityRepository(db: WorkoutTrackerDb): EntityRepository {
  return {
    async getAll(): Promise<ReadonlyArray<DbEntity>> {
      const [error, entities] = await tryCatch(
        db.entities.orderBy('createdAt').reverse().toArray(),
      )
      if (error) {
        throw createDatabaseError('LOAD_FAILED', 'retrieve entities', error)
      }
      return entities
    },

    async getById(id: string): Promise<DbEntity | undefined> {
      const [error, entity] = await tryCatch(db.entities.get(id))
      if (error) {
        throw createDatabaseError('LOAD_FAILED', `retrieve entity with id ${id}`, error)
      }
      return entity
    },

    async create(
      entity: Omit<DbEntity, 'id' | 'createdAt'>,
    ): Promise<DbEntity> {
      const newEntity: DbEntity = {
        ...entity,
        id: generateId(),
        createdAt: Date.now(),
      }

      const [error] = await tryCatch(db.entities.add(newEntity))
      if (error) {
        throw createDatabaseError('SAVE_FAILED', 'create entity', error)
      }

      return newEntity
    },

    async update(
      id: string,
      updates: Partial<Omit<DbEntity, 'id' | 'createdAt'>>,
    ): Promise<void> {
      const [error, updatedCount] = await tryCatch(
        db.entities.update(id, {
          ...updates,
          updatedAt: Date.now(),  // Auto-inject timestamp
        }),
      )

      if (error) {
        throw createDatabaseError('SAVE_FAILED', `update entity with id ${id}`, error)
      }

      if (updatedCount === 0) {
        throw createDatabaseError('NOT_FOUND', `entity with id ${id} not found`)
      }
    },

    async delete(id: string): Promise<void> {
      const [error] = await tryCatch(db.entities.delete(id))
      if (error) {
        throw createDatabaseError('SAVE_FAILED', `delete entity with id ${id}`, error)
      }
      // Soft delete: no NOT_FOUND check
    },
  }
}
关键模式:
  • 对所有操作使用
    tryCatch()
    包装器(推荐模式)
  • 两阶段错误检查:操作失败 + 未找到实体
  • 自动注入时间戳:
    createdAt
    updatedAt
  • 使用
    generateId()
    生成新ID
  • 软删除:如果实体不存在则不抛出错误

Step 5: Register in Factory Provider

步骤5:在工厂提供者中注册

Location:
src/db/implementations/dexie/index.ts
Import and add to provider:
typescript
import { createDexieEntityRepository } from './entity'

export function createDexieRepositoryProvider(): RepositoryProvider {
  return {
    activeWorkout: createDexieActiveWorkoutRepository(db),
    workouts: createDexieWorkoutsRepository(db),
    // ... existing repositories
    entity: createDexieEntityRepository(db),  // ADD THIS
  }
}
位置:
src/db/implementations/dexie/index.ts
导入并添加至提供者:
typescript
import { createDexieEntityRepository } from './entity'

export function createDexieRepositoryProvider(): RepositoryProvider {
  return {
    activeWorkout: createDexieActiveWorkoutRepository(db),
    workouts: createDexieWorkoutsRepository(db),
    // ... existing repositories
    entity: createDexieEntityRepository(db),  // ADD THIS
  }
}

Step 6: Export Public Getter

步骤6:导出公共获取器

Location:
src/db/index.ts
Add getter function:
typescript
export function getEntityRepository(): EntityRepository {
  return getRepositoryProvider().entity
}
位置:
src/db/index.ts
添加获取器函数:
typescript
export function getEntityRepository(): EntityRepository {
  return getRepositoryProvider().entity
}

Usage in Features

在功能模块中使用

typescript
import { getEntityRepository } from '@/db'
import type { DbEntity } from '@/db/schema'

export function useEntities() {
  const entities = ref<ReadonlyArray<DbEntity>>([])
  const entityRepo = getEntityRepository()

  async function loadEntities() {
    entities.value = await entityRepo.getAll()
  }

  async function createEntity(name: string, value: string | null) {
    const newEntity = await entityRepo.create({ name, value, updatedAt: null })
    entities.value = [...entities.value, newEntity]
  }

  async function updateEntity(id: string, updates: Partial<DbEntity>) {
    await entityRepo.update(id, updates)
    await loadEntities()
  }

  async function deleteEntity(id: string) {
    await entityRepo.delete(id)
    entities.value = entities.value.filter(e => e.id !== id)
  }

  onMounted(() => loadEntities())

  return {
    entities: readonly(entities),
    createEntity,
    updateEntity,
    deleteEntity,
  }
}
typescript
import { getEntityRepository } from '@/db'
import type { DbEntity } from '@/db/schema'

export function useEntities() {
  const entities = ref<ReadonlyArray<DbEntity>>([])
  const entityRepo = getEntityRepository()

  async function loadEntities() {
    entities.value = await entityRepo.getAll()
  }

  async function createEntity(name: string, value: string | null) {
    const newEntity = await entityRepo.create({ name, value, updatedAt: null })
    entities.value = [...entities.value, newEntity]
  }

  async function updateEntity(id: string, updates: Partial<DbEntity>) {
    await entityRepo.update(id, updates)
    await loadEntities()
  }

  async function deleteEntity(id: string) {
    await entityRepo.delete(id)
    entities.value = entities.value.filter(e => e.id !== id)
  }

  onMounted(() => loadEntities())

  return {
    entities: readonly(entities),
    createEntity,
    updateEntity,
    deleteEntity,
  }
}

Key Principles

核心原则

1. Error Handling

1. 错误处理

Preferred: tryCatch wrapper
typescript
const [error, result] = await tryCatch(operation)
if (error) {
  throw createDatabaseError('ERROR_CODE', 'description', error)
}
Error codes:
  • SAVE_FAILED
    - Create, update, delete operations
  • LOAD_FAILED
    - Read operations
  • NOT_FOUND
    - Entity doesn't exist
推荐:tryCatch包装器
typescript
const [error, result] = await tryCatch(operation)
if (error) {
  throw createDatabaseError('ERROR_CODE', 'description', error)
}
错误码:
  • SAVE_FAILED
    - 创建、更新、删除操作
  • LOAD_FAILED
    - 读取操作
  • NOT_FOUND
    - 实体不存在

2. Timestamp Management

2. 时间戳管理

Auto-inject timestamps in repository methods:
  • createdAt: Date.now()
    in
    create()
  • updatedAt: Date.now()
    in
    update()
  • lastUsedAt: Date.now()
    when accessing entity
在仓库方法中自动注入时间戳:
  • create()
    中设置
    createdAt: Date.now()
  • update()
    中设置
    updatedAt: Date.now()
  • 访问实体时设置
    lastUsedAt: Date.now()

3. ID Generation

3. ID生成

Always use
generateId()
from
database.ts
:
typescript
import { generateId } from './database'

const newEntity = {
  ...entity,
  id: generateId(),  // crypto.randomUUID()
}
始终使用
database.ts
中的
generateId()
typescript
import { generateId } from './database'

const newEntity = {
  ...entity,
  id: generateId(),  // crypto.randomUUID()
}

4. Soft Delete

4. 软删除

Delete operations don't throw if entity doesn't exist:
typescript
async delete(id: string): Promise<void> {
  await tryCatch(db.entities.delete(id))
  // No NOT_FOUND check - silent success
}
删除操作在实体不存在时不抛出错误:
typescript
async delete(id: string): Promise<void> {
  await tryCatch(db.entities.delete(id))
  // No NOT_FOUND check - silent success
}

5. Type Safety

5. 类型安全

  • Use
    Readonly<T>
    and
    ReadonlyArray<T>
    for function parameters
  • Use
    Omit<>
    to exclude auto-generated fields
  • Use discriminated unions with exhaustive checking
  • Define type guards for runtime type checking
  • 对函数参数使用
    Readonly<T>
    ReadonlyArray<T>
  • 使用
    Omit<>
    排除自动生成的字段
  • 使用可区分联合并进行穷尽检查
  • 定义类型守卫以进行运行时类型检查

File Reference

文件参考

Critical files when creating repository:
  • src/db/interfaces.ts
    - Interface definition + RepositoryProvider
  • src/db/schema.ts
    - Db-prefixed type definitions
  • src/db/implementations/dexie/database.ts
    - Table + indexes
  • src/db/implementations/dexie/[entity].ts
    - Implementation
  • src/db/implementations/dexie/index.ts
    - Factory registration
  • src/db/index.ts
    - Public getter export
Utility imports:
  • @/lib/tryCatch
    - Error handling utilities
  • @/db/implementations/dexie/database
    - generateId()
创建仓库时的关键文件:
  • src/db/interfaces.ts
    - 接口定义 + RepositoryProvider
  • src/db/schema.ts
    - 带Db前缀的类型定义
  • src/db/implementations/dexie/database.ts
    - 表 + 索引
  • src/db/implementations/dexie/[entity].ts
    - 实现代码
  • src/db/implementations/dexie/index.ts
    - 工厂注册
  • src/db/index.ts
    - 公共获取器导出
工具导入:
  • @/lib/tryCatch
    - 错误处理工具
  • @/db/implementations/dexie/database
    - generateId()

Detailed References

详细参考

For complete examples and advanced patterns, see:
  • references/examples.md - Complete end-to-end examples:
    • Example 1: Simple CRUD repository (Notes)
    • Example 2: Complex transformations (Tags with many-to-many)
    • Example 3: Extending Settings with function overloads
  • references/patterns.md - Detailed pattern catalog:
    • Error handling patterns (direct throw vs tryCatch)
    • CRUD patterns (getAll, create, update, delete, timestamps)
    • Type transformation patterns (helper utilities, deep cloning)
    • Advanced patterns (function overloads, singleton, transactions, bulk ops)
    • Schema design patterns (discriminated unions, indexing, embedded vs referenced)
如需完整示例和高级模式,请查看:
  • references/examples.md - 完整端到端示例:
    • 示例1:简单CRUD仓库(笔记)
    • 示例2:复杂转换(多对多标签)
    • 示例3:通过函数重载扩展设置
  • references/patterns.md - 详细模式目录:
    • 错误处理模式(直接抛出 vs tryCatch)
    • CRUD模式(getAll、create、update、delete、时间戳)
    • 类型转换模式(辅助工具、深度克隆)
    • 高级模式(函数重载、单例、事务、批量操作)
    • 模式设计模式(可区分联合、索引、嵌入式 vs 引用式)

Common Scenarios

常见场景

Scenario 1: Simple CRUD Repository

场景1:简单CRUD仓库

Need basic storage for an entity? See examples.md → Example 1 (Notes).
Quick checklist:
  1. Define interface with getAll/getById/create/update/delete
  2. Add DbEntity type with Db prefix
  3. Add table with indexes
  4. Implement using tryCatch pattern
  5. Register and export
需要为实体提供基础存储?请查看examples.md → 示例1(笔记)
快速检查清单:
  1. 定义包含getAll/getById/create/update/delete的接口
  2. 添加带Db前缀的DbEntity类型
  3. 添加带索引的表
  4. 使用tryCatch模式实现
  5. 注册并导出

Scenario 2: Complex Relationships

场景2:复杂关联

Need many-to-many relationships or complex queries? See examples.md → Example 2 (Tags).
Pattern: Junction table + transaction handling + usage tracking.
需要多对多关联或复杂查询?请查看examples.md → 示例2(标签)
模式: 关联表 + 事务处理 + 使用情况追踪。

Scenario 3: Extending Settings

场景3:扩展设置

Adding new setting? See examples.md → Example 3.
Pattern: Add discriminated union member + function overload + default value.
添加新设置?请查看examples.md → 示例3
模式: 添加可区分联合成员 + 函数重载 + 默认值。

Scenario 4: Conversions Between Types

场景4:类型间转换

Need to convert between templates and workouts? See patterns.md → Type Transformation Patterns.
Pattern: Helper utilities with exhaustive switch statements.
需要在模板和健身记录之间转换?请查看patterns.md → 类型转换模式
模式: 使用穷尽switch语句的辅助工具。

Scenario 5: Bulk Operations

场景5:批量操作

Import/export or batch delete? See patterns.md → Advanced Patterns → Bulk Operations.
Pattern: Transactions +
Promise.all()
+
bulkAdd()
.
导入/导出或批量删除?请查看patterns.md → 高级模式 → 批量操作
模式: 事务 +
Promise.all()
+
bulkAdd()

Testing Support

测试支持

Mock repositories for unit tests:
typescript
import { createMockRepositories } from '@/__tests__/helpers/mockRepositories'

const mockRepos = createMockRepositories()
mockRepos.entity.getAll.mockResolvedValue([...])
Integration tests with fake-indexeddb are automatically set up via test helpers.
用于单元测试的模拟仓库:
typescript
import { createMockRepositories } from '@/__tests__/helpers/mockRepositories'

const mockRepos = createMockRepositories()
mockRepos.entity.getAll.mockResolvedValue([...])
通过测试助手自动设置使用fake-indexeddb的集成测试。

Migration Strategy

迁移策略

When updating schema version:
  1. Increment version number in
    database.ts
  2. Add new table/indexes in
    .stores({})
  3. Dexie handles migrations automatically
  4. For data migrations, use
    .upgrade()
    callback
typescript
this.version(3)
  .stores({
    entities: 'id, name, createdAt',
  })
  .upgrade(tx => {
    // Optional data migration logic
    return tx.table('entities').toCollection().modify(entity => {
      entity.newField = 'default'
    })
  })
更新模式版本时:
  1. database.ts
    中递增版本号
  2. .stores({})
    中添加新表/索引
  3. Dexie自动处理迁移
  4. 如需数据迁移,使用
    .upgrade()
    回调
typescript
this.version(3)
  .stores({
    entities: 'id, name, createdAt',
  })
  .upgrade(tx => {
    // Optional data migration logic
    return tx.table('entities').toCollection().modify(entity => {
      entity.newField = 'default'
    })
  })

Project-Specific Repositories

项目特定仓库

Db*
Types vs Domain Types

Db*
类型 vs 领域类型

AspectDatabase (
Db*
)
Domain
File
src/db/schema.ts
src/types/
Prefix
DbWorkout
,
DbSet
Workout
,
Set
No value
null
undefined
Optimized forStorageApp logic
方面数据库(
Db*
领域
文件
src/db/schema.ts
src/types/
前缀
DbWorkout
DbSet
Workout
Set
无值表示
null
undefined
优化方向存储应用逻辑

Available Repositories

可用仓库

SettingsRepository - Key-value store with defaults:
ts
const repo = getSettingsRepository()
await repo.get('theme')           // 'light' | 'dark' | 'system'
await repo.get('defaultRestTimer') // number
await repo.set({ key: 'theme', value: 'dark' })
await repo.getAll()               // All settings merged with defaults
await repo.reset('theme')
CustomExercisesRepository - Exercise CRUD:
ts
const repo = getCustomExercisesRepository()
await repo.getAll()
await repo.getById(id)
await repo.add({ id: generateId(), name: 'Squat', ... })
await repo.update(id, { name: 'Back Squat' })
await repo.delete(id)
WorkoutsRepository - Completed workouts:
ts
const repo = getWorkoutsRepository()
await repo.getAll()
await repo.getById(id)
await repo.create(convertWorkoutToDb(workout))
await repo.delete(id)
ActiveWorkoutRepository - Singleton active workout:
ts
const repo = getActiveWorkoutRepository()
await repo.load()
await repo.save(dbActiveWorkout)
await repo.delete()
await repo.exists()
BenchmarksRepository - Benchmark workouts:
ts
const repo = getBenchmarksRepository()
await repo.getAll()
await repo.getById(id)
await repo.create({ id: generateId(), name: 'Fran', ... })
await repo.update(id, { name: 'Fran (Scaled)' })
await repo.delete(id)
TemplatesRepository - Workout templates:
ts
const repo = getTemplatesRepository()
await repo.getAll()
await repo.getById(id)
await repo.create(template)
await repo.update(id, changes)
await repo.delete(id)
SettingsRepository - 带默认值的键值存储:
ts
const repo = getSettingsRepository()
await repo.get('theme')           // 'light' | 'dark' | 'system'
await repo.get('defaultRestTimer') // number
await repo.set({ key: 'theme', value: 'dark' })
await repo.getAll()               // 合并默认值后的所有设置
await repo.reset('theme')
CustomExercisesRepository - 自定义练习CRUD:
ts
const repo = getCustomExercisesRepository()
await repo.getAll()
await repo.getById(id)
await repo.add({ id: generateId(), name: 'Squat', ... })
await repo.update(id, { name: 'Back Squat' })
await repo.delete(id)
WorkoutsRepository - 已完成的健身记录:
ts
const repo = getWorkoutsRepository()
await repo.getAll()
await repo.getById(id)
await repo.create(convertWorkoutToDb(workout))
await repo.delete(id)
ActiveWorkoutRepository - 单例当前健身记录:
ts
const repo = getActiveWorkoutRepository()
await repo.load()
await repo.save(dbActiveWorkout)
await repo.delete()
await repo.exists()
BenchmarksRepository - 基准健身记录:
ts
const repo = getBenchmarksRepository()
await repo.getAll()
await repo.getById(id)
await repo.create({ id: generateId(), name: 'Fran', ... })
await repo.update(id, { name: 'Fran (Scaled)' })
await repo.delete(id)
TemplatesRepository - 健身记录模板:
ts
const repo = getTemplatesRepository()
await repo.getAll()
await repo.getById(id)
await repo.create(template)
await repo.update(id, changes)
await repo.delete(id)

Using Converters

使用转换器

Always convert when crossing domain/database boundary:
ts
import { convertWorkoutToDb, convertDbToWorkout } from '@/db/converters'

// Domain → Database
const dbWorkout = convertWorkoutToDb(workout)
await getWorkoutsRepository().create(dbWorkout)

// Database → Domain
const dbWorkout = await getWorkoutsRepository().getById(id)
const workout = convertDbToWorkout(dbWorkout)
在领域层与数据库层之间转换时务必使用转换器:
ts
import { convertWorkoutToDb, convertDbToWorkout } from '@/db/converters'

// 领域 → 数据库
const dbWorkout = convertWorkoutToDb(workout)
await getWorkoutsRepository().create(dbWorkout)

// 数据库 → 领域
const dbWorkout = await getWorkoutsRepository().getById(id)
const workout = convertDbToWorkout(dbWorkout)

Partial Updates with buildPartialUpdate

使用buildPartialUpdate进行部分更新

Dexie's
update()
overwrites all keys in the object. Use
buildPartialUpdate
to only modify provided fields:
ts
import { buildPartialUpdate } from '@/db/partialUpdate'

const NULLABLE_FIELDS = ['equipment', 'muscle', 'image']

// Only includes keys present in updates
// Converts undefined → null for nullable fields
const dbUpdates = buildPartialUpdate(updates, NULLABLE_FIELDS)
await repo.update(id, dbUpdates)
Why: Without filtering,
{ name: 'Squat', equipment: undefined }
would set equipment to null even if you only meant to update the name.
Dexie的
update()
会覆盖对象中的所有键。使用
buildPartialUpdate
仅修改指定字段:
ts
import { buildPartialUpdate } from '@/db/partialUpdate'

const NULLABLE_FIELDS = ['equipment', 'muscle', 'image']

// 仅包含更新中存在的键
// 可为空字段将undefined转换为null
const dbUpdates = buildPartialUpdate(updates, NULLABLE_FIELDS)
await repo.update(id, dbUpdates)
原因: 如果不进行过滤,
{ name: 'Squat', equipment: undefined }
会将equipment设置为null,即使你只想更新名称。

Project-Specific Gotchas

项目特定注意事项

1. Use
null
in Database,
undefined
in Domain

1. 数据库中使用
null
,领域中使用
undefined

IndexedDB doesn't support
undefined
:
ts
// Database types
type DbExercise = {
  equipment: Equipment | null  // Use null
}

// Domain types
type Exercise = {
  equipment?: Equipment  // Use undefined
}
IndexedDB不支持
undefined
ts
// 数据库类型
type DbExercise = {
  equipment: Equipment | null  // 使用null
}

// 领域类型
type Exercise = {
  equipment?: Equipment  // 使用undefined
}

2. Always Reset Database in Tests

2. 测试中务必重置数据库

ts
import { resetDatabase } from '@/__tests__/setup'

beforeEach(async () => {
  await resetDatabase()
})
ts
import { resetDatabase } from '@/__tests__/setup'

beforeEach(async () => {
  await resetDatabase()
})

3. Convert Types at Boundaries

3. 在边界处转换类型

ts
// BAD - Type mismatch
await getWorkoutsRepository().create(workout)

// GOOD - Convert first
const dbWorkout = convertWorkoutToDb(workout)
await getWorkoutsRepository().create(dbWorkout)
ts
// 错误 - 类型不匹配
await getWorkoutsRepository().create(workout)

// 正确 - 先转换
const dbWorkout = convertWorkoutToDb(workout)
await getWorkoutsRepository().create(dbWorkout)