Package and version
@prisma-next/postgres@0.14.0 (and @prisma-next/sql-orm-client@0.14.0)
What happened?
On a namespace-qualified Postgres ORM read (db.orm.public.<Model>), .include('<relation>') resolves the related rows to { [x: string]: unknown }[] instead of the related model's row type. Base scalar reads on the same collection type correctly — only the eager-loaded relation loses its field types. (Runtime data is correct; the breakage is static-types-only.)
What did you expect to happen?
db.orm.public.Author.where(...).include('posts').first() should type posts as the Post row type (e.g. { id: string; title: string; authorId: string }[]), the same way DefaultModelRow<Contract, 'Post'> resolves standalone.
Minimal reproduction
Contract (single public namespace, a 1:N relation):
namespace public {
model Author {
id String @id
name String
posts Post[]
@@map("author")
}
model Post {
id String @id
title String
authorId String
author Author @relation(fields: [authorId], references: [id])
@@map("post")
}
}
import postgres from '@prisma-next/postgres/runtime'
import type { Contract } from './contract'
import contractJson from './contract.json' with { type: 'json' }
const db = postgres<Contract>({ contractJson, url: 'postgresql://USER:PASS@HOST/DB' })
const row = await db.orm.public.Author.where((a) => a.id.eq('x')).include('posts').first()
// row.posts → { [x: string]: unknown }[] ❌ expected { id: string; title: string; authorId: string }[]
const base = await db.orm.public.Author.where((a) => a.id.eq('x')).first()
// base.name → string ✅ base scalars resolve fine
Root cause (traced in @prisma-next/sql-orm-client@0.14.0/dist/index.d.mts)
The include path threads a branded namespace literal into the related-row resolution, and TypeScript rejects a branded intersection as an index key:
db.orm.public correctly sets the collection's State['nsId'] = 'public' (index.d.mts:1292-1295).
.include (index.d.mts:725) computes the related row as InferRootRow<TContract, RelatedName, TargetNs> with TargetNs = RelationTargetNamespace<…, State['nsId']>.
- Because
nsId is not never, RelationTargetNamespace (index.d.mts:540-544) infers TargetNs from the relation's stored to.namespace.
- In the emitted
contract.d.ts, to.namespace is 'public' & NamespaceId where NamespaceId = string & { readonly __brand: 'NamespaceId' } — a branded type, not the bare literal 'public'.
NamespaceModelsOf (index.d.mts:324) does NsId extends keyof namespaces ? namespaces[NsId] : …. The branded 'public' & NamespaceId passes extends keyof (assignable to 'public'), but namespaces['public' & NamespaceId] is error TS2538: Type cannot be used as an index type. The indexed access degrades to Record<string, never>, so FieldsOf/ModelDef (index.d.mts:326, 341) are empty and DefaultModelRow's mapped type (index.d.mts:277) collapses to { [x: string]: unknown }.
Verified with type probes against a real contract:
DefaultModelRow<Contract, 'Post', never> // ✅ full row (this is why standalone DbDoc works)
DefaultModelRow<Contract, 'Post', 'public'> // ✅ full row (bare literal)
DefaultModelRow<Contract, 'Post', 'public' & NamespaceId> // ❌ { [x: string]: unknown } (what include threads)
Base reads work because they resolve at NsId = never (the ScannedModelDef all-namespaces scan, index.d.mts:325), which never hits the branded index access.
Suggested fix
Normalize NsId to a bare key before the indexed access — either strip the brand in RelationTargetNamespace / NamespaceModelsOf (e.g. NsId extends infer K extends keyof namespaces ? namespaces[K] : …, or map the branded literal back to its bare form), or emit to.namespace as a plain string literal rather than 'public' & NamespaceId.
Workaround
Cast the eager-loaded result to a DbDoc<>-derived row type, which resolves correctly at NsId = never:
type AuthorWithPosts = DbDoc<'Author'> & { readonly posts: readonly DbDoc<'Post'>[] }
const row = (await db.orm.public.Author.where((a) => a.id.eq('x')).include('posts').first()) as unknown as AuthorWithPosts | null
A branch refinement (.include('posts', (p) => p.select('id','title','authorId'))) recovers the selected fields' types but still merges in the broken { [x: string]: unknown } index signature, so it is only a partial mitigation.
Environment
- Node: v22.20.0
- OS: darwin 26.3
- Package manager: bun 1.3.14
- Database: Postgres (target
@prisma-next/target-postgres@0.14.0)
Additional context
Surfaced upgrading a per-service multi-database Postgres monorepo from 0.13 → 0.14 (the qualify-flat-builder-accessors change made all reads db.orm.public.<Model>, which is what exposes the branded-namespace include path). Single public schema per service; every .include() call site is affected.
Package and version
@prisma-next/postgres@0.14.0(and@prisma-next/sql-orm-client@0.14.0)What happened?
On a namespace-qualified Postgres ORM read (
db.orm.public.<Model>),.include('<relation>')resolves the related rows to{ [x: string]: unknown }[]instead of the related model's row type. Base scalar reads on the same collection type correctly — only the eager-loaded relation loses its field types. (Runtime data is correct; the breakage is static-types-only.)What did you expect to happen?
db.orm.public.Author.where(...).include('posts').first()should typepostsas thePostrow type (e.g.{ id: string; title: string; authorId: string }[]), the same wayDefaultModelRow<Contract, 'Post'>resolves standalone.Minimal reproduction
Contract (single
publicnamespace, a 1:N relation):Root cause (traced in
@prisma-next/sql-orm-client@0.14.0/dist/index.d.mts)The include path threads a branded namespace literal into the related-row resolution, and TypeScript rejects a branded intersection as an index key:
db.orm.publiccorrectly sets the collection'sState['nsId'] = 'public'(index.d.mts:1292-1295)..include(index.d.mts:725) computes the related row asInferRootRow<TContract, RelatedName, TargetNs>withTargetNs = RelationTargetNamespace<…, State['nsId']>.nsIdis notnever,RelationTargetNamespace(index.d.mts:540-544) infersTargetNsfrom the relation's storedto.namespace.contract.d.ts,to.namespaceis'public' & NamespaceIdwhereNamespaceId = string & { readonly __brand: 'NamespaceId' }— a branded type, not the bare literal'public'.NamespaceModelsOf(index.d.mts:324) doesNsId extends keyof namespaces ? namespaces[NsId] : …. The branded'public' & NamespaceIdpassesextends keyof(assignable to'public'), butnamespaces['public' & NamespaceId]iserror TS2538: Type cannot be used as an index type. The indexed access degrades toRecord<string, never>, soFieldsOf/ModelDef(index.d.mts:326, 341) are empty andDefaultModelRow's mapped type (index.d.mts:277) collapses to{ [x: string]: unknown }.Verified with type probes against a real contract:
Base reads work because they resolve at
NsId = never(theScannedModelDefall-namespaces scan,index.d.mts:325), which never hits the branded index access.Suggested fix
Normalize
NsIdto a bare key before the indexed access — either strip the brand inRelationTargetNamespace/NamespaceModelsOf(e.g.NsId extends infer K extends keyof namespaces ? namespaces[K] : …, or map the branded literal back to its bare form), or emitto.namespaceas a plain string literal rather than'public' & NamespaceId.Workaround
Cast the eager-loaded result to a
DbDoc<>-derived row type, which resolves correctly atNsId = never:A branch refinement (
.include('posts', (p) => p.select('id','title','authorId'))) recovers the selected fields' types but still merges in the broken{ [x: string]: unknown }index signature, so it is only a partial mitigation.Environment
@prisma-next/target-postgres@0.14.0)Additional context
Surfaced upgrading a per-service multi-database Postgres monorepo from 0.13 → 0.14 (the
qualify-flat-builder-accessorschange made all readsdb.orm.public.<Model>, which is what exposes the branded-namespace include path). Singlepublicschema per service; every.include()call site is affected.