Previously the hook hard-coded a `{ items: Array<...> }` cache contract,
so the two resources actually shipping OrderEndpoints — /pins and
/groups — would have thrown `length mismatch` on first drag because
their GET responses are flat arrays (Pin[], Group[]).
Auto-detect flat arrays and `.items`-bearing objects; expose paired
selectItems/updateItems accessors for nested shapes (grouped views,
GraphQL-style connections, envelopes with a different field name).
Half-configured accessors throw at hook construction.
Degradation splits into two tiers: "cache not loaded" (all paths no-op
and warn per call) vs "shape unrecognized" — move/applyBatch let the
PATCH through since id + anchor are self-contained, while
applyReorderedList refuses since blind whole-list reorder without a
client baseline is unsafe. Unrecognized-shape warnings are
de-duplicated per hook instance.
Paginated shapes (OffsetPaginationResponse / CursorPaginationResponse)
now preserve non-items metadata (total, page, nextCursor) on optimistic
overlays.
Update the guide to match what's now landed in code:
- §2: drop the `reserved groupId = '__pinned__'` bucket teaching — pin
is a separate table, not an overloaded scope value. Point readers
to the schema/service JSDoc for resource-design details.
- §3: add applyScopedMoves to the helper table.
- §3.1 (new): document the scoped-reorder contract (service-side
scope inference, multi-scope batch rejection, missing-id semantics)
in one short section.
- §9 "Group Ordering" upgraded from future-extension placeholder to
spec, trimmed to ordering-relevant points only.
- §10 "Pin Ordering" added, also ordering-only. Polymorphic shape,
purgeForEntity contract, idempotent pin semantics, and hard-delete
rationale are deliberately NOT duplicated here — they live in
pin.ts / PinService.ts JSDoc where the code is.
- Partition-dimensions list and migration checklist updated so
`group.entityType` / `pin.entityType` show as live rather than
hypothetical.
Add `.notNull()` to `createdAt` / `updatedAt` in the shared
`createUpdateTimestamps` helper so Drizzle `$inferSelect` produces
`number` instead of the misleading `number | null`. `deletedAt` in
`createUpdateDeleteTimestamps` stays nullable (soft-delete semantics).
Generated migration 0013 rebuilds 26 affected tables via the standard
SQLite table-recreation pattern; FK / CHECK / INDEX constraints are
preserved across rebuild. No backfill is added (project is in the
development phase; null pre-existing rows are accepted as a "wipe DB"
signal rather than engineered around).
Fix upstream in `KnowledgeMappings.toTimestamp` so it returns a
`Date.now()` fallback instead of `undefined` — otherwise future Dexie
-> v2 migrator runs would try to insert undefined into NOT NULL
columns. Three test assertions updated from `undefined` to
`expect.any(Number)`.
Sweep 18 downstream call sites across 9 functions that were carrying a
dead `?? new Date().toISOString()` fallback:
- AssistantService, KnowledgeBaseService, KnowledgeItemService,
MessageService, TopicService, TranslateHistoryService,
TranslateLanguageService (the original Pattern A set)
- McpServerService and MiniAppService.rowToMiniApp (reclassified from
Pattern B: the domain types stay `optional` to accommodate builtin
literals in the renderer, but `string` assigns legally into
`string | undefined`, so the switch is safe)
Keep `MiniAppService.builtinToMiniApp` on `timestampToISOOrUndefined`
— its `dbRow?: MiniAppSelect` semantics ("the preference row may not
exist at all") is genuinely optional, not a disguised "nullable column".
Also remove a Pattern C that neither the plan nor the grep audit
caught: `TagService.ensureTagTimestamp` was a self-rolled defense
layer that threw INTERNAL_SERVER_ERROR on null timestamps. The DB
now refuses to produce such rows, so the defense — and the test
named "should surface timestamp anomalies instead of masking them" —
are dead code. Removed both.
Update three docs to reflect the new defaults:
- `services/utils/README.md` — drop the "DB still nullable" table row
and the predictive paragraph; reframe Pattern B around
"whole row may not exist"
- `services/utils/rowMappers.ts` JSDoc — same reframing
- `docs/references/data/data-api-in-main.md` — delete the fallback
code samples and simplify Convention §3
Introduce shared `services/utils/rowMappers.ts` exporting three helpers:
`nullsToUndefined` (renamed from the previous `stripNulls`, with a
corrected type signature that preserves `notNull()` columns unchanged),
`timestampToISO` for guaranteed-present timestamps, and
`timestampToISOOrUndefined` for nullable ones.
Migrate 9 services off hand-rolled null/date handling:
- Eliminate the `stripNulls` duplicate in MiniAppService
- Replace 18 repetitive createdAt/updatedAt ternaries with helper calls
- Fix McpServerService misuse where an optional domain field was being
forced to a synthesized "now" value; restore honest undefined semantics
- Simplify KnowledgeBaseService.rowToKnowledgeBase via the spread idiom,
bypassing `clean` for the `T | null` embeddingModelId field
Document the paradigm in docs/references/data/data-api-in-main.md with
standard and advanced skeletons, plus an explicit "when NOT to spread"
list covering services with field renaming, custom fallbacks, computed
fields, or sensitive-data sanitization. Per-helper design decisions
(shallow vs. recursive, rejected alternatives) live in
services/utils/README.md.
Extend the renderer data hooks to cover three mutation shapes that
previously had to drop to imperative dataApiService calls:
- Template paths (e.g. `/providers/:providerId`) with a runtime `params`
option on useQuery / useMutation / useInfiniteQuery / usePaginatedQuery,
so a single hook instance can operate on ids chosen at call time.
ParamsForPath derives types directly from the schema's existing
`params: {...}` declarations (no template-string parsing).
- Function-form `refresh: ({ args, result }) => ConcreteApiPaths[]` for
invalidation keys that depend on trigger input or server response. Args
are closure-captured at trigger entry to avoid races between concurrent
calls.
- Explicit `/*` suffix for path-segment prefix matching on refresh and
invalidate patterns, preserving the trailing slash so `/providers/*`
doesn't match siblings like `/providers-archived`.
A single `resolveTemplate` function is the canonical path-replacement
point, so `useQuery('/providers/:id', { params: { id: 'abc' } })` and
`useQuery('/providers/abc')` produce byte-for-byte identical cache keys.
Dev-mode assertions flag invalid `/*` patterns (bare wildcard or missing
trailing slash) and warn on concurrent triggers against the same template
hook instance, which would share SWR mutation state.
Fully backward compatible: existing `refresh: ['/topics']` and
concrete-path hook calls compile and behave identically.
Unify the feature under its canonical name "Quick Assistant":
- Rename miniWindow.html → quickAssistant.html and
windows/mini/ → windows/quickAssistant/ (via git mv to preserve history).
The bounds file miniWindow-state.json is not migrated; users see default
bounds once after upgrade.
- Collapse half-renamed QuickWindow/MiniWindow identifiers in
QuickAssistantService and its callers (ShortcutService, TrayService,
ShortcutService.test) onto QuickAssistant.
- Rename the Inputbar scope literal 'mini-window' → 'quick-assistant'.
The Redux DEFAULT_TOOL_ORDER_BY_SCOPE keeps the legacy key behind a
transitional Record<InputbarScope | 'mini-window', ToolOrder> annotation;
both entries will be collapsed when Redux is removed.
- Rename i18n keys miniwindow.*, mini_window, show_mini_window to
quickAssistant.*, quick_assistant, show_quick_assistant across all
locales; synced via i18n:sync.
- Update lingering "mini window" wording in docs and comments.
Legacy 'mini_window' literals intentionally retained in the v2
ShortcutMappings migrator (identifies Redux-persisted old keys) and in
v1 Redux store files (to be removed with Redux).
Extend the DataApi router's `extractPathParams` to recognize `:name*` segments
as greedy captures that consume one or more path segments joined with `/`.
Greedy params may appear as the last segment or in the middle anchored by
trailing static / plain-param segments; at most one greedy is allowed per
pattern. No decoding is performed, consistent with the existing raw-string
policy.
This unblocks routing for composite identifiers whose values contain `/`
(e.g. OpenRouter-style `qwen/qwen3-vl`, Fireworks-style
`accounts/fireworks/models/deepseek-v3p2`) without resorting to URL encoding,
which the project does not use.
- Add 21 unit tests covering plain params, tail greedy, middle greedy,
multi-greedy rejection, and edge cases.
- Document the feature under "Greedy Path Parameters" in the API design
guidelines with tail/middle/mixed examples.
Introduce src/main/data/db/sqliteErrors.ts with three APIs:
- classifySqliteError(e): walks the .cause chain and classifies UNIQUE /
FOREIGN KEY / CHECK / NOT NULL violations, including PRIMARYKEY and
ROWID codes which SQLite reports as semantically UNIQUE.
- withSqliteErrors(op, handlers): runs op and routes recognized
violations through a sparse handlers map. Constraint kinds without a
handler and non-SQLite errors are rethrown unchanged by construction,
so "forgot to rethrow" is not representable in client code.
- defaultHandlersFor(resource, identifier): complete default handler set
for the common CRUD case. Spread to override specific kinds.
Migrate TranslateLanguageService.create and MiniAppService.create to the
new API. The MiniAppService migration also removes a select-then-insert
TOCTOU race: two concurrent create requests with the same appId now
produce one 201 and one 409 instead of one 201 and one 500.
Delete the now-unused errorUtils.ts. Document the module in
src/main/data/db/README.md and api-design-guidelines.md with a note that
withSqliteErrors handlers are a TOCTOU fallback, not a replacement for
application-level pre-validation.
Drizzle's `casing: 'snake_case'` config only applies to the ORM channel.
Raw SQL via `db.all(sql`...`)` returns SQLite's native snake_case columns
with no runtime mapping — the TypeScript generic on `db.all<T>()` is a
compile-time assertion only. The recursive CTEs in `getTree`,
`getBranchMessages`, and `getPathToNode` used `SELECT *` and asserted
results to `messageTable.$inferSelect` (camelCase), so downstream code
silently read `undefined` from `parentId` and `siblingsGroupId`. This
broke multi-model message grouping whenever the renderer rebuilt
branches from the database (page refresh, topic switch).
Switch to the "CTE for IDs, ORM for rows" pattern: the recursive CTE
collects IDs only (single-word columns are casing-safe), then full rows
are fetched via `db.select().from(messageTable).where(inArray(...))`
where Drizzle applies camelCase mapping automatically. CTE order is
restored via a Map since SQL IN-list does not preserve order. Rename
`tree_depth` to `treeDepth` for naming consistency.
Document the pattern as the project standard in
`docs/references/data/database-patterns.md` and add regression tests
covering all three methods plus the multi-model siblings scenario.
IpcAdapter is a transport adapter bridging Electron IPC to the
transport-agnostic ApiServer. Previously it managed its own lifecycle
via manual setupHandlers()/removeHandlers() calls, bypassing
BaseService's Disposable tracking.
Now IpcAdapter implements Disposable (setup/dispose), and
DataApiService registers it via registerDisposable() — eliminating
manual onStop() cleanup and gaining exception-safe teardown via
BaseService's finally block. This also paves the way for future
HttpAdapter to follow the same pattern.
Signed-off-by: fullex <0xfullex@gmail.com>
Align with the existing @logger alias convention by introducing
@application as a short alias for src/main/core/application. This
reduces import verbosity across ~130 main-process files while keeping
4 intentional sub-path imports (@main/core/application/Application)
unchanged to preserve the vi.mock bypass mechanism in tests.
Configured in tsconfig.node.json and electron.vite.config.ts; Vitest
inherits the alias automatically.
Signed-off-by: fullex <0xfullex@gmail.com>
Replace the ad-hoc seeding system with a journal-based architecture that
tracks seed versions via app_state table and skips unchanged seeds on startup.
- Introduce ISeeder interface with name/version/description/run() contract
- Add SeedRunner orchestrator with journal-based version tracking
- Rename ISeed -> ISeeder, migrate() -> run() (align with industry conventions)
- Rename *Seed -> *Seeder classes, *Seeding.ts -> *Seeder.ts files
- Move seeders into seeding/seeders/ subdirectory for better organization
- Add hashObject utility for auto-computing version from static data sources
- PreferenceSeeder/TranslateLanguageSeeder: auto checksum via hashObject()
- PresetProviderSeeder: lazy getter using RegistryLoader.getProvidersVersion()
- Simplify DbService.onInit() to single SeedRunner.runAll() call
- Add SeedRunner tests and PreferenceSeeder tests
- Add database-seeding-guide.md with version strategy documentation
Signed-off-by: fullex <0xfullex@gmail.com>
### What this PR does
Before this PR:
- v2 migration did not include provider/model data migration from legacy
`llm` state.
- Provider/model data APIs and handlers were incomplete.
- `@cherrystudio/provider-registry` (formerly provider-catalog) package
was not integrated into the data layer.
After this PR:
- Add provider/model migration path (`ProviderModelMigrator` + mappings)
and register it in v2 migrator flow.
- Add `@cherrystudio/provider-registry` package with JSON-based registry
data, Zod-validated schemas, and lifecycle-managed
`ProviderRegistryService`.
- Complete provider/model schemas, services, handlers, shared API
schemas/types, and model merger utility.
- Complete provider API endpoints (`registry-models`, `auth-config`,
`api-keys`) aligned with lifecycle DI patterns.
**Note:** This PR is intentionally scoped to backend/data-layer only.
Renderer consumer migration will be submitted in a separate PR to
maintain domain separation.
Fixes #
N/A
### Why we need it and why it was done in this way
The following tradeoffs were made:
- Kept migration and data API implementation within current v2
architecture (handler -> service -> db schema) instead of adding
temporary compatibility layers.
- Replaced protobuf toolchain with JSON + Zod validation for simpler
data pipeline and better debuggability.
- Converted all numeric enums to string-valued `as-const` objects
(EndpointType, ModelCapability, Modality, etc.) for runtime
debuggability.
- Unified separate `baseUrls`, `modelsApiUrls`, `reasoningFormatTypes`
fields into a single `endpointConfigs` map keyed by EndpointType.
The following alternatives were considered:
- Keep protobuf-based registry data; rejected due to complexity of proto
toolchain and poor debuggability of binary data.
- Include renderer consumer migration in same PR; deferred to separate
PR for cleaner domain boundaries.
Links to places where the discussion took place:
- Original combined PR: #14034
### Breaking changes
None.
### Special notes for your reviewer
- This is a backend-only extraction from #14034, which contained both
backend and renderer consumer code. The renderer migration will follow
in a separate PR.
- Please focus review on migration flow (`ProviderModelMigrator`),
provider/model service contracts, and the registry package design.
- The `@cherrystudio/provider-registry` package was renamed from
`provider-catalog` and uses JSON data files instead of protobuf.
### Checklist
- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: Write code that humans can understand and Keep it simple
- [x] Refactor: You have left the code cleaner than you found it (Boy
Scout Rule)
- [ ] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [ ] Documentation: Not required (internal data layer, no user-facing
changes)
- [x] Self-review: I have reviewed my own code
### Release note
```release-note
NONE
```
---------
Signed-off-by: jidan745le <420511176@qq.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Co-authored-by: suyao <sy20010504@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
Services now handle both business logic and data access directly via Drizzle ORM.
Repository pattern is strongly discouraged unless absolutely necessary.
Signed-off-by: fullex <0xfullex@gmail.com>
Enforce a linear upgrade path (v1.last → v2.0.0 → v2.x) before
allowing data migration to proceed. Users who manually install v2
bypassing the auto-updater are now blocked at startup if their
previous version is incompatible.
- Add versionPolicy.ts with pure check function and version.log reader
- Add versionLogFile to MigrationPaths (uses resolved userData path)
- Wire version check into v2MigrationGate after needsMigration()
- Block on: no version.log, v1 too old (<1.9.0), v2.0.0 gateway skipped
- Treat v2.0.0 pre-releases as "before v2.0.0" per semver ordering
- Update migration docs with version upgrade requirements
Signed-off-by: fullex <0xfullex@gmail.com>
v1 users who configured a custom userData directory via
~/.cherrystudio/config/config.json had their data silently skipped
during v2 migration, because resolveUserDataLocation() reads only
boot-config.json (empty on first v2 launch) and the migration
pipeline ran at the wrong default path.
Introduce MigrationPaths — a frozen, pre-computed path object
resolved once at the migration gate entry. All 10 scattered
app.getPath('userData') and path.join calls in migration/v2 are
replaced with centralized constants. Legacy config.json detection
runs before engine initialization, redirecting both the internal
paths and Chromium-level storage (via app.setPath) so the migration
window's renderer can access the correct Dexie/localStorage data.
When the detected custom path is inaccessible (e.g. unmounted
external drive), a dialog prompts the user instead of silently
falling back to an empty default directory.
Signed-off-by: fullex <0xfullex@gmail.com>