Skip to content

adapter-store: sanctioned single-resource fetch by string key (route-binding keys) #119

@jasperboerhof

Description

@jasperboerhof

Problem

adapter-store consumers can only get a single resource into the store via retrieveById(id: number). For Laravel backends using custom route-model binding, the show route often accepts a string key as well — e.g. kendo's Issue::resolveRouteBinding resolves both numeric ids and issue keys, so GET projects/1/issues/KD-0123 and GET projects/1/issues/123 both work.

Because the store has no typed path for string keys, kendo's issue Show/Edit pages currently work around it by calling retrieveAll() and resolving the key client-side — fetching 600+ issues in production to display one, on every page mount (retrieveAll has no caching). We want to fix that, and the fix needs a sanctioned way to fetch one resource by key.

Constraints we want to keep:

  • setById stays internal — the public StoreModuleForAdapter surface remains read-only.
  • No type-system abuse in consumers (retrieveById(slug as unknown as number) works at runtime since the value is only URL-interpolated, but we don't want to ship that).

Options

Option 1 — first-class retrieveByKey(key: string)

A sibling of retrieveById, identical implementation:

retrieveByKey: async (key: string) => {
    const { data } = await httpService.getRequest(`${domainName}/${key}`);
    setById(data);
},
  • Laravel route-model binding with custom keys is a first-class framework concept, so "fetch one resource by string key" arguably belongs in the package's domain rather than being app-specific.
  • setById stays internal, numeric call sites keep strict number typing.
  • Misuse surface: calling it on a store whose backend only binds ids yields a 404 — the same failure mode as retrieveById(999999) today.
  • Narrow: solves exactly this need and nothing else; a future store-level need means another package change.

Option 2 — generic extend config hook

Capability injection in the style of the existing broadcast config: the hook runs once at store creation and receives the same internal module tier the adapter factory already gets.

type AdapterStoreConfig<T, E, N, X extends object = {}> = {
    // ...existing...
    broadcast?: AdapterStoreBroadcast<T>;
    extend?: (storeModule: AdapterStoreModule<T>) => X;
};
// createAdapterStoreModule returns StoreModuleForAdapter<T, E, N> & X

Consumer side:

extend: ({setById}) => ({
    retrieveBySlug: async (slug: string): Promise<void> => {
        const {data} = await httpService.getRequest<IssueResource>(`${url}/${slug}`);
        setById(data);
    },
}),
  • Most future-proof: consumers define their own sanctioned store-level methods without further package changes, and nothing app-specific (like "slug") enters the package.
  • The trust boundary is unchanged in kind — adapter and broadcast.subscribe already receive the internal tier at creation time; extend is the same pattern generalized.
  • It does relax the current discipline that custom writes are adapter methods tied to a resource — store-level custom methods become a thing. Whether that's a direction the package wants is the main question here.

Option 3 — widen retrieveById to number | string

Smallest possible diff. Trade-off is semantic: a method named ById accepting keys blurs what actually binds, and every store starts accepting strings whether or not its backend supports them.

Option 4 — no package change (consumer-side resolve-then-retrieve)

The consumer does a plain getRequest on the show route to learn the numeric id, then calls the typed retrieveById(data.id):

  • numeric value → 1 request (straight retrieveById)
  • string key → 2 requests, the second re-fetching a payload the consumer already had, because retrieveById is the only sanctioned door into the store

Zero package work, fully typed, mildly wasteful by design. Also viable as an interim while one of the other options is discussed/released, since the consumer-side fix isn't blocked on a package release.

Ask

Which direction fits the package best? Happy to PR whichever option we agree on.

Context: found while fixing kendo's issue Show page perf (600-issue fetch to render one issue).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions