Repository navigation
Replies: 1 comment
|
Design PR #4100 proposes |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Summary
Today,
referencefields returned bygetEmDashCollection()/getEmDashEntry()are plain ID strings (zod-generator.ts:120,fields/reference.ts). Resolving them requires a follow-upgetEmDashEntry()call per row, which becomes the textbook N+1 problem when rendering list views (e.g. a blog index that wants the author name, a product list that needs the brand name).I'd like to propose a small, additive query option —
depth— that eagerly hydratesreferencefields in batch, with no impact on existing call sites.Why
Concrete case: I'm migrating a 9-collection / ~30k-entry sakepo product catalog from Strapi to EmDash. The catalog renders an
/liquorsindex page (100 items per page) where each card shows the product name + producer name. With Strapi'spopulate=producerthis is one query. With the current EmDash query API the choices are:None of these scale, and all of them push complexity into every consumer of the query API. This is the kind of thing the framework should solve once.
EmDash already has
hydrateEntryBylinesandgetBylinesForEntrieswhich solve the exact same problem for byline credits — the proposal is to generalize that pattern to user-definedreferencefields.Proposed API (additive)
Two complementary options on
CollectionFilter/getEmDashEntryoptions:Type changes:
Behavior
depth: 0(default): no behavior change. Reference fields stay as ID strings.depth: N: walk the result rows, collect every reference-typed field, batch-fetch the targets in a singleIN (...)query per target collection (chunked to respect D1's variable limit — see below), substitute objects in place, and recurseN - 1times on the newly hydrated children.null(matches Strapi/Payload behavior).depthso no infinite recursion.Out of scope for v1 (could be discussed/added later):
populateobject form. v1 landsdepthonly;populateis the granular successor that we can iterate on oncedepthis in.select: [...]).where: { producer: { country: "JP" } }).Implementation sketch (v1:
depthonly)The change is concentrated and reuses the existing batch-fetch pattern.
packages/core/src/loader.ts— extendCollectionFilterwithdepth?: numberand thread it throughloadCollection/loadEntry. After mapping rows to entries, call a newhydrateReferences(type, entries, depth)helper.packages/core/src/query.ts—getEmDashCollection/getEmDashEntryacceptdepth, pass through to the loader, and skip the helper whendepth === 0(zero overhead default path).packages/core/src/query/hydrate-references.ts:_emdash_fieldsonce via the schema registry to get(collection, field) → { type: "reference", target_collection }map (cached per request via existinggetRequestContext()).Map<targetCollection, Set<referencedId>>.SELECT * FROM ec_<target> WHERE id IN (...)per chunk (chunk size = 90 to stay under D1's 100-variable limit, matching the workaround needed for issue #219).Map<id, row>and substitute into the parentdata.depth > 1, recurse on the hydrated child rows.The whole thing should be ~200-300 lines including tests.
Interaction with existing features
hydrateEntryBylinesruns afterloadCollection, references would too. Order can be parallel (Promise.all) since they touch different tables.localecontext as the parent query. Easy via the existing ALS context.status === "published"unless preview mode is active (same rule as the parent fetch). Dangling refs to draft/unpublished targets becomenull.entry.edit): I propose v1 does not wrap hydrated children with edit proxies, since they originate from a different collection. That can be added later if there's demand.Type generation
emdash-env.d.tscurrently generatesreferencefields asstring(zod-generator.ts:366). For v1 I'd leave that as-is (so existing code keeps compiling) and let consumers cast where they need the hydrated shape:Improving generated types so
depth: 1returnsLiquor & { producer: Producer }is doable but TypeScript-heavy and probably worth its own follow-up PR.Alternatives considered
Comparison with peers
populate=*, deeply nested object formdepth: number(docs)*[_type=="post"]{..., author->}?fields=*,author.*Payload's
depthis closest in spirit to what I'm proposing here — minimal API surface, sane default, easy to reason about.Questions for maintainers
depthAPI the right shape, or do you prefer to start withpopulatedirectly? My case fordepthfirst: it's small, opt-in, and unblocks the common case immediately.populatecan be layered on without breaking it.depthmatches Payload;populate: true | objectmatches Strapi. I'm flexible.getRequestContext()'s edit-mode wrapping, or stay raw?What I'm offering
If this gets a thumbs up, I'd open a PR with:
loader.ts/query.tschangeshydrate-references.tsmodule mirroring the byline patternpackages/core/test/covering: zero-depth no-op, single ref, multi ref, multi collection, dangling ref, depth=2 recursion, IN-clause chunking, locale propagation.changeset/minor-version entrydocs/src/content/docs/guides/querying-content.mdxHappy to iterate on the API in this thread before writing any code.
(AI disclosure: this proposal was drafted with Claude Code. Implementation, tests, and review will be human-checked against the existing patterns in
bylines/index.tsand follow the same style.)All reactions