Skip to content

feat(firestore): type cyclic DocumentReference fields through Collection Keys - #198

Merged
Kaeldehta merged 2 commits into
mainfrom
fix/recursive-schemas
Nov 10, 2026
Merged

Kaeldehta merged 2 commits into
mainfrom
fix/recursive-schemas

Conversation

@Kaeldehta

Copy link
Copy Markdown
Collaborator

Summary

A model can hold a typed Document Reference into its own collection, into a subcollection of it, or into a collection whose model references it back. The cyclic field names its target with a Collection Key (ADR 0004):

+interface UsersKey extends Collection.Key<typeof Users> {}
+
 class User extends FirestoreModel.Class<User>("User")({
   name: Schema.String,
-  bestFriend: Schema.NullOr(FirestoreSchema.DocumentReference(Users)), // TS2310, TS2506, TS7022
+  bestFriend: Schema.NullOr(
+    Schema.suspend(
+      (): FirestoreSchema.DocumentReference<UsersKey> => FirestoreSchema.DocumentReference(Users),
+    ),
+  ),
 }) {}
 const Users = Collection.make("users", User);

u.bestFriend is a Collection.DocumentReference<typeof Users>. The key only resolves at the type level:

FirestoreSchema.DocumentReference<X>        X = collection | Collection.Key
  Schema.declare<ResolvedReference<X>>      extra interface keeps the cycle lazy
    Collection.DocumentReference<CollectionOf<X>>
      CollectionOf<UsersKey>      = typeof Users   (read from the key's member)
      CollectionOf<typeof Users>  = typeof Users   (identical to before)

Fields that close no cycle are unchanged, and nothing changes at runtime. Closes #197.

Evidence

  • Before: the self-reference above fails to type-check:
    error TS2310: Type 'User' recursively references itself as a base type
    error TS2506: 'User' is referenced directly or indirectly in its own base expression
    error TS7022: 'Users' implicitly has type 'any' because it ... is referenced directly or indirectly in its own initializer
    
    After: TSTyche tests in typetest/FirestoreSchema.tst.ts pass for a self-reference, a parent referencing its own subcollection, a mutual cycle, and one model backing two collections. Each covers the getDoc read type, an updateDoc patch and a rejected wrong patch. A key that names the wrong collection is rejected, including a sibling collection with the same model. FirestoreSchema.DocumentReference<typeof Users>["Type"] is still exactly Collection.DocumentReference<typeof Users>. A runtime test writes and reads back a self-reference and a subcollection reference, and both are Equal to Users.doc(...) / Posts.doc(...).
    vp run check       pass
    vp run test-types  477 passed, 126 @ts-expect-error matched
    vp test (core)     456 passed
    
    Client and admin tests fail at emulator startup (code.charCodeAt is not a function) with or without this change, so none of their tests run.

Merge Danger

Door: two-way

Collection.Key is new @stability unstable API. The changeset bumps every package to 0.4.0.

Blast Radius: types

Only types change. Existing FirestoreSchema.DocumentReference(C) fields keep their exact types.

…ion Keys

A model can now hold a typed FirestoreSchema.DocumentReference into its own
collection, a subcollection of it, or a collection whose model references it
back. Collection.Key names the target collection, and
FirestoreSchema.DocumentReference accepts either a collection or a key,
resolving both to the same Collection.DocumentReference.

Closes #197
@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Explain Change →

No actionable comments were generated in the recent review.

Summary by CodeRabbit

  • New Features
    • Firestore document references can now be typed for models that reference documents in their own collection, a subcollection, or another collection that references them back.
    • References retain their collection-specific types, including when collections share a model.
  • Documentation
    • Added guidance and examples for defining typed references in cyclic models.

Walkthrough

The change adds Collection.Key and allows FirestoreSchema.DocumentReference to accept a collection key or a collection. Keyed references resolve to the corresponding collection reference type. Documentation describes using an annotated Schema.suspend callback for cyclic fields. Type tests cover reference typing and update validation; runtime tests check encoding and decoding keyed references.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: Minimal · up to 703e5

No identified issue needs to be resolved before merging, subject to normal checks.

Pre-merge checks: Passed 5

Check name Status Explanation
Title check Passed The title "feat(firestore): type cyclic DocumentReference fields through Collection Keys" names the main change: typing self-referencing and cyclic DocumentReference fields via the new Collection.Key …
Description check Passed The description explains the cyclic Document Reference use case, shows a before and after code example, describes the Collection.Key type-level resolution, lists test evidence, and notes the unstable …
Linked Issues check Passed Issue #197 is the only direct linked issue and is open. Its requirements are coding requirements: support typed DocumentReference fields into the model's own collection (bestFriend case) and into a su…
Out of Scope Changes check Passed Every changed file serves the Collection Key objective of #197. Changes: Collection.ts (new Key interface); FirestoreSchema.ts (type-level DocumentReference and docs); internal/firestoreSchema.ts (cas…
Docstring Coverage Passed No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Full details: Title check

Explanation

The title "feat(firestore): type cyclic DocumentReference fields through Collection Keys" names the main change: typing self-referencing and cyclic DocumentReference fields via the new Collection.Key API. The changeset, ADR, Collection.ts, FirestoreSchema.ts and tests all support this.

Full details: Description check

Explanation

The description explains the cyclic Document Reference use case, shows a before and after code example, describes the Collection.Key type-level resolution, lists test evidence, and notes the unstable API and type-only impact. It matches the changeset.

Full details: Linked Issues check

Explanation

Issue #197 is the only direct linked issue and is open. Its requirements are coding requirements: support typed DocumentReference fields into the model's own collection (bestFriend case) and into a subcollection (pinnedPost case), plus type tests in packages/effect-firebase/typetest/ for both cases. The diff meets these. FirestoreSchema.ts now types DocumentReference<C> over Collection.Key<any> through ResolvedReference/CollectionOf. Collection.ts adds the exported Collection.Key interface. typetest/FirestoreSchema.tst.ts adds cases for a self-reference (Friend.bestFriend), a subcollection reference (Author.pinned into Notes = Authors.subcollection), a mutual cycle, and two collections sharing one model. Each case checks the read type, getDoc and updateDoc results, and wrong-patch rejection. Wrong-key rejections are also covered. The issue lists the key approach as a possible direction and does not select one, so the chosen design is within scope. The issue's unannotated Schema.suspend reproduction is not expected to compile unchanged. The approach requires a Collection.Key annotation on cyclic fields, which the issue allows. Limit: I did not run the test suites. The pass rests on the reviewed source and the author's reported results (vp run test-types: 477 passed; vp test (core): 456 passed). The runtime test in FirestoreSchema.test.ts was not inspected.

Full details: Out of Scope Changes check

Explanation

Every changed file serves the Collection Key objective of #197. Changes: Collection.ts (new Key interface); FirestoreSchema.ts (type-level DocumentReference and docs); internal/firestoreSchema.ts (cast through unknown in the DocumentReference constructor, which is type-only and leaves the schema validation and codec unchanged); typetest and FirestoreSchema.test.ts (coverage for the objective); docs/adr/0004 and GLOSSARY.md (document the new key concept); and .changeset/collection-keys.md (release note for the new API). The changeset's version bump to 0.4.0 is release metadata for the new unstable API. It is not an unrelated code change. No demonstrated unrelated change was found.

Full details: Docstring Coverage

Explanation

No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 0 functions across 5 files. (3 skipped: 3 unsupported.)

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 2ed6733e-9fb6-4a2b-bf5b-c65d9387e61b
Commits

Reviewing files that changed from the base of the PR and between 415d03b and 703e571.

Files selected for processing: 8

  • .changeset/collection-keys.md
  • GLOSSARY.md
  • docs/adr/0004-cyclic-references-go-through-collection-keys.md
  • packages/effect-firebase/src/firestore/Collection.ts
  • packages/effect-firebase/src/firestore/FirestoreSchema.ts
  • packages/effect-firebase/src/firestore/internal/firestoreSchema.ts
  • packages/effect-firebase/tests/firestore/FirestoreSchema.test.ts
  • packages/effect-firebase/typetest/FirestoreSchema.tst.ts

Included review availability: This review used your included allowance. 2 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 3 reviews per hour.

Recent review details

Context from checks skipped due to timeout: 4

  • GitHub Check: Check (format, lint, types)
  • GitHub Check: Test (Node 22)
  • GitHub Check: Test (Node 24)
  • GitHub Check: Test types
Additional context used

Code guidelines: 4

AGENTS.md — auto-discovered
packages/effect-firebase/README.md — configured
docs/agents/domain.md — auto-discovered
GLOSSARY.md — auto-discovered
Path-based instructions: 4

Source excerpt: [ ] Write a negative type test as `// `@ts-expect-error` ` directly above the bad statement (prefix it with `void` if it is an Effect or Stream), not as `toRaiseError`: `vp check` type-checks `typetest/` too...

CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/effect-firebase/typetest/FirestoreSchema.tst.ts
Source excerpt: Typed Firestore collections and Firebase services for [Effect](https://effect.website), independent of any Firebase SDK.

CodeRabbit inference engine (packages/effect-firebase/README.md)

Files:

  • packages/effect-firebase/tests/firestore/FirestoreSchema.test.ts
  • packages/effect-firebase/src/firestore/Collection.ts
  • packages/effect-firebase/src/firestore/internal/firestoreSchema.ts
  • packages/effect-firebase/typetest/FirestoreSchema.tst.ts
  • packages/effect-firebase/src/firestore/FirestoreSchema.ts
Applicability reference: AGENTS.md: # Using Vite+, the Unified Toolchain for the Web Single-context: one `GLOSSARY.md` and `docs/adr/` at the repo root.

CodeRabbit inference engine (docs/agents/domain.md)

Files:

  • docs/adr/0004-cyclic-references-go-through-collection-keys.md
  • GLOSSARY.md
Applicability reference: AGENTS.md: # Using Vite+, the Unified Toolchain for the Web Single-context: one `GLOSSARY.md` and `docs/adr/` at the repo root.

CodeRabbit inference engine (GLOSSARY.md)

Files:

  • docs/adr/0004-cyclic-references-go-through-collection-keys.md
  • GLOSSARY.md
Learnings: 2

Learning: 2026-11-04T14:47:55.067Z

Learnt from: CR
Repo: VisioLab/effect-firebase PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-11-04T14:47:55.067Z
Learning: The API reference on the website (`apps/website`) is generated from the JSDoc of every public module, including the documented members of exported interfaces and classes. Write each export's comment the way Effect writes its own (see `node_modules/effect/src/Effect.ts`), in this order:

````ts
/**
 * One-sentence summary of what it is or does.
 *
 * **When to use**
 *
 * Use to … / Use when … — one or two sentences on when to pick this over the alternatives.
 *
 * **Details**
 *
 * Behaviour, edge cases, failure modes, short paragraphs. Leave out for simple exports.
 *
 * example Writing a document
 * ```ts
 * import { Effect } from "effect";
 * import { Firestore } from "visiolab/effect-firebase/firestore";
 * // …
 * ```
 *
 * see {link addDoc} for writing under an id Firestore picks
 *
 * stability unstable
 * category constructors
 * since 0.2.0
 */
````

Applied to files:

  • packages/effect-firebase/src/firestore/Collection.ts
Learning: 2026-10-31T21:48:11.752Z

Learnt from: CR
Repo: VisioLab/effect-firebase PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-10-31T21:48:11.752Z
Learning: Applies to packages/effect-firebase/typetest/FirestoreSchema.tst.ts : Source excerpt:
# Using Vite+, the Unified Toolchain for the Web

## Project Review Checklist

- [ ] Write a negative type test as `// ts-expect-error <message fragment>` directly above the bad statement (prefix it with `void` if it is an Effect or Stream), not as `toRaiseError`: `vp check` type-checks `typetest/` too and would report the error, while TSTyche still matches the fragment against the suppressed diagnostic.

Applied to files:

  • packages/effect-firebase/typetest/FirestoreSchema.tst.ts
LanguageTool

docs/adr/0004-cyclic-references-go-through-collection-keys.md

[style] ~10-~10: The double modal “needs finished” is nonstandard (only accepted in certain dialects). Consider “to be finished”.
Context: ...y, and the constant's type needs User finished. - **A global registry from path string...

(NEEDS_FIXED)

Additional comments: 7

packages/effect-firebase/src/firestore/Collection.ts: 1

290-344: LGTM!

packages/effect-firebase/src/firestore/FirestoreSchema.ts: 1

192-207: LGTM!

packages/effect-firebase/typetest/FirestoreSchema.tst.ts: 1

84-295: LGTM!

GLOSSARY.md: 1

37-39: LGTM!

.changeset/collection-keys.md: 1

1-5: LGTM!

packages/effect-firebase/src/firestore/internal/firestoreSchema.ts: 1

52-54: LGTM!

Also applies to: 89-89

packages/effect-firebase/tests/firestore/FirestoreSchema.test.ts: 1

225-262: LGTM!

@Kaeldehta
Kaeldehta merged commit c1a602b into main Nov 10, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A model cannot hold a DocumentReference into its own collection or a subcollection of it

1 participant