feat(admin): generic resource collection data layer (all content domains)#2764
Open
Innei wants to merge 15 commits into
Open
feat(admin): generic resource collection data layer (all content domains)#2764Innei wants to merge 15 commits into
Innei wants to merge 15 commits into
Conversation
Non-React kernel for the generic resource-collection data layer: stable list-key serialization, per-entity base/overlay collection store with optimistic update/insert/delete lifecycle and version tracking, and a multi-op transaction with partial-success commit/rollback.
…vation errorsByKey used to gain a key with an undefined value whenever a transaction rolled back an unfulfilled op without an error; only write the key when an error is actually present. Also drop the now-unreachable overlay guard in get()'s update branch, and extract the immer produce+patch-derivation logic shared by collection.ts and transaction.ts into deriveUpdate(). Adds tests for the errorsByKey fix, a version bump across a successful commit, and a transaction update with partial fulfilledKeys.
Adds list-index.ts (hydrateList/readList/touchList with a per-collection 50-key LRU cap) and hooks.ts (useEntity, useEntityList, useCollectionListQuery, useCollectionDetailQuery) to apps/admin/src/data/resource/. Extends CollectionState<T> with listIndexes, keeping React Query as a transport-only layer that stores hydration receipts.
Route the admin category list/detail/mutation flow through the new resource-collection kernel (apps/admin/src/data/resource/) instead of raw useQuery + list-cache lookups. Adds apps/admin/src/data/resources/category.ts defining the categories collection (optimistic update/delete backed by updateCategory/deleteCategory), and retypes the category feature's props/context from CategoryModel to the collection's CategoryEntity. Tag handling is untouched (tags are not a collection yet); post list wiring inside category detail stays on useQuery pending Task 4.
Route the admin post list/detail/write/mutation flows through the resource-collection kernel instead of raw useQuery + list-cache lookups. - Add apps/admin/src/data/resources/post.ts: the posts collection with a normalize that mirrors the embedded category snapshot into the categories collection, and patchPost/deletePost persistence. - use-posts-list: posts + category-filter lists via useCollectionListQuery + useEntityList; keepPrevious replaces placeholderData. - use-post-mutations: publish/pin/category via posts.update, delete via posts.delete, batch delete via a transaction with fulfilledKeys partial commit/rollback. Toasts and invalidatePosts preserved. - CategoryDetail/TagDetail post lists move onto the posts collection. - Write page (post kind only): split detail into useCollectionDetailQuery + useEntity; seed the form at most once per (kind,id) so background hydration cannot wipe in-progress edits; save-edit path applies an optimistic transaction and reconciles the server echo via hydrate. note/page paths unchanged. Relax ResourceTransaction.commit to return the request result so the write path can reconcile the server echo; fulfilledKeys detection unchanged.
…ng posts store Register every defineCollection() instance in a module-level registry and expose resetAllCollections(), called from all three sign-out paths (sidebar logout, change-password sign-out, session self-revoke) so stale entity state doesn't survive an account switch. Split usePostsList's keyword-search branch off the shared posts collection: search results (decorated with translated title/lang/ isFallback/highlight) now flow through a plain useQuery scoped to the Query cache instead of collection.hydrate(), so they can no longer pollute the canonical posts store or leak into the write form.
SafeDep Report SummaryPackage Details
This report is generated by SafeDep Github App |
… update saveTopic's edit branch went through PATCH (204, no body), so the client's optimistic value was committed without ever seeing the server's slugify() normalization. Mirror savePost: explicit transaction + PUT round-trip that hydrates the collection with the real server echo. Also switch topic.ts's onUpdate from patchTopic to updateTopic (PUT) since no remaining caller uses topics.update() directly, following the category.ts precedent.
…on before optimistic updates - Give TopicFormModal's prefill query its own key so it never collides with the receipt-producing useCollectionDetailQuery consumers (TopicDetail, TopicDetailRoute), which was causing the edit form to transiently render empty and clobber typed input on refill. - Ensure post/note entities are hydrated into their collections before registering optimistic update ops, fixing "update on unknown entity" thrown by search-mode row actions (publish/pin/move/patch) where the search branch doesn't hydrate collections. - Give the posts search query its own key, matching the notes convention, instead of sharing the list key with the receipt-producing query. - Use literal kinds for the three write.detail query keys so disabled detail observers target distinct cache entries instead of collapsing onto the active kind's key. - Remove topic.ts's dead onUpdate handler (no callers; edit path uses an explicit PUT transaction instead).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




Summary
Production implementation of the resource-store idea validated by the POC in #2741: a generic normalized client store for admin entities, with React Query demoted to pure transport and optimistic writes as kernel-guaranteed transactions. Design spec:
docs/superpowers/specs/2026-07-10-admin-resource-collection-data-layer-design.md.All content domains are migrated: category, post, topic, note, page, say, recently, project, link. Comments are deferred (nested/stateful shape needs its own design).
Kernel (
apps/admin/src/data/resource/)createTransactionfor multi-op/batch writes with partial-success semantics (fulfilledKeys).useEntity,useEntityList(keepPreviouspagination),useCollectionListQuery/useCollectionDetailQuery/useCollectionInfiniteQuery(cursor lists hydrate via list-index append mode). Only{ hydratedAt }receipts enter the Query cache.resetAllCollections()wired into every sign-out path.Per-domain mutation modules (
apps/admin/src/data/resources/)*.mutations.tsown transaction composition, optimistic patches, persistence calls, and server-echo reconcile as plain async functions; the feature layer keeps onlyuseMutationshells, toasts, invalidations, and navigation. Highlights:(kind, id)form-seed guard — background list hydrations can never reset in-progress edits.setQueryDatacache surgery with a plain entity upsert.Verification
pnpm -C apps/admin exec vitest run src/data/resource/ src/features/write/— 49/49pnpm -C apps/admin run typecheck,pnpm -C apps/admin run build, oxlint on all changed files — cleanSupersedes #2741.
Follow-ups (non-blocking, tracked in review notes)
batchRemovekernel helper (3× duplication)pageParam != nullcheckPostRowcategory via hook-layer join (optimistic category rename on rows)oxlintto devDependencies; LRU active-key eviction guardPartialCategorySchemafirestypedefault on partial updates; duplicate slug returns 500 instead of 409