From dc9edcec35f9b937410289425998abdeeba6046d Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 31 Mar 2026 19:13:49 +0200 Subject: [PATCH 1/4] feat(sql-orm-client): expand and simplify output types Closes: https://linear.app/prisma-company/issue/TML-2139/prettify-query-result-types-with-recursive-mapped-type Query result types in `sql-orm-client` are currently unexpanded intersections full of internal utility types, making them hard to read in IDE tooltips and error messages. This change applies a recursive mapped type at the top-level return positions so that IDE tooltips and hover types display fully-evaluated object literals. --- .../sql-orm-client/src/collection.ts | 55 +++--- .../sql-orm-client/src/grouped-collection.ts | 11 +- .../3-extensions/sql-orm-client/src/types.ts | 12 ++ .../sql-orm-client/test/simplify-deep.test.ts | 166 ++++++++++++++++++ 4 files changed, 219 insertions(+), 25 deletions(-) create mode 100644 packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts diff --git a/packages/3-extensions/sql-orm-client/src/collection.ts b/packages/3-extensions/sql-orm-client/src/collection.ts index fc3b29ff68..d1a334cbb5 100644 --- a/packages/3-extensions/sql-orm-client/src/collection.ts +++ b/packages/3-extensions/sql-orm-client/src/collection.ts @@ -107,6 +107,7 @@ import { type RelationNames, type ResolvedCreateInput, type ShorthandWhereFilter, + type SimplifyDeep, type UniqueConstraintCriterion, type VariantModelRow, type VariantNames, @@ -159,7 +160,7 @@ interface MtiCreateContext { export class Collection< TContract extends Contract, ModelName extends string, - Row = InferRootRow, + Row = SimplifyDeep>, State extends CollectionTypeState = DefaultCollectionTypeState, > implements RowSelection { @@ -317,15 +318,17 @@ export class Collection< ): Collection< TContract, ModelName, - Row & { - [K in RelName]: IncludeRefinementValue< - TContract, - ModelName, - K, - DefaultModelRow, - RefinedResult - >; - }, + SimplifyDeep< + Row & { + [K in RelName]: IncludeRefinementValue< + TContract, + ModelName, + K, + DefaultModelRow, + RefinedResult + >; + } + >, State > { const relation = resolveIncludeRelation(this.contract, this.modelName, relationName as string); @@ -391,15 +394,17 @@ export class Collection< }; return this.#cloneWithRow< - Row & { - [K in RelName]: IncludeRefinementValue< - TContract, - ModelName, - K, - DefaultModelRow, - RefinedResult - >; - }, + SimplifyDeep< + Row & { + [K in RelName]: IncludeRefinementValue< + TContract, + ModelName, + K, + DefaultModelRow, + RefinedResult + >; + } + >, State >({ includes: [...this.state.includes, includeExpr], @@ -416,15 +421,19 @@ export class Collection< ): Collection< TContract, ModelName, - Pick, Fields[number]> & - IncludedRelationsForRow, + SimplifyDeep< + Pick, Fields[number]> & + IncludedRelationsForRow + >, State > { const selectedFields = mapFieldsToColumns(this.contract, this.modelName, fields); return this.#cloneWithRow< - Pick, Fields[number]> & - IncludedRelationsForRow, + SimplifyDeep< + Pick, Fields[number]> & + IncludedRelationsForRow + >, State >({ selectedFields, diff --git a/packages/3-extensions/sql-orm-client/src/grouped-collection.ts b/packages/3-extensions/sql-orm-client/src/grouped-collection.ts index 4a23e5667c..89df648d2f 100644 --- a/packages/3-extensions/sql-orm-client/src/grouped-collection.ts +++ b/packages/3-extensions/sql-orm-client/src/grouped-collection.ts @@ -21,6 +21,7 @@ import type { DefaultModelRow, HavingBuilder, HavingComparisonMethods, + SimplifyDeep, } from './types'; import { combineWhereExprs } from './where-utils'; @@ -84,7 +85,11 @@ export class GroupedCollection< async aggregate( fn: (aggregate: AggregateBuilder) => Spec, ): Promise< - Array, GroupFields[number]> & AggregateResult> + Array< + SimplifyDeep< + Pick, GroupFields[number]> & AggregateResult + > + > > { const aggregateSpec = fn(createAggregateBuilder(this.contract, this.modelName)); const aggregateEntries = Object.entries(aggregateSpec); @@ -118,7 +123,9 @@ export class GroupedCollection< } return mapped; }) as Array< - Pick, GroupFields[number]> & AggregateResult + SimplifyDeep< + Pick, GroupFields[number]> & AggregateResult + > >; } } diff --git a/packages/3-extensions/sql-orm-client/src/types.ts b/packages/3-extensions/sql-orm-client/src/types.ts index 3a76d01700..46122bf82a 100644 --- a/packages/3-extensions/sql-orm-client/src/types.ts +++ b/packages/3-extensions/sql-orm-client/src/types.ts @@ -22,6 +22,18 @@ import type { ExecutionContext } from '@prisma-next/sql-relational-core/query-la import type { ComputeColumnJsType } from '@prisma-next/sql-relational-core/types'; import type { RowSelection } from './collection-internal-types'; +// --------------------------------------------------------------------------- +// SimplifyDeep — recursive type prettifier for IDE tooltips +// --------------------------------------------------------------------------- + +export type SimplifyDeep = T extends readonly (infer Element)[] + ? SimplifyDeep[] + : T extends string | number | boolean | bigint | symbol | Date | Uint8Array + ? T + : T extends object + ? { [K in keyof T]: SimplifyDeep } + : T; + // --------------------------------------------------------------------------- // Comparison / Filter / Order / Include // --------------------------------------------------------------------------- diff --git a/packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts b/packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts new file mode 100644 index 0000000000..c09a5dab18 --- /dev/null +++ b/packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts @@ -0,0 +1,166 @@ +import { describe, expectTypeOf, test } from 'vitest'; +import { Collection } from '../src/collection'; +import type { SimplifyDeep } from '../src/types'; +import { createMockRuntime, getTestContext } from './helpers'; + +describe('SimplifyDeep', () => { + test('primitives pass through', () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test('branded primitives pass through', () => { + type Branded = string & { readonly __brand: true }; + expectTypeOf>().toEqualTypeOf(); + }); + + test('Date and Uint8Array preserved', () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test('intersections flatten into plain objects', () => { + type Input = { a: number } & { b: string }; + type Expected = { a: number; b: string }; + expectTypeOf>().toEqualTypeOf(); + }); + + test('arrays recurse', () => { + type Input = ({ a: number } & { b: string })[]; + type Expected = { a: number; b: string }[]; + expectTypeOf>().toEqualTypeOf(); + }); + + test('nested objects recurse', () => { + type Input = { nested: { a: number } & { b: string } }; + type Expected = { nested: { a: number; b: string } }; + expectTypeOf>().toEqualTypeOf(); + }); + + test('nullable objects', () => { + type Input = ({ a: number } & { b: string }) | null; + type Expected = { a: number; b: string } | null; + expectTypeOf>().toEqualTypeOf(); + }); + + test('nested arrays of intersected objects', () => { + type Input = { + items: ({ id: number } & { name: string })[]; + }; + type Expected = { + items: { id: number; name: string }[]; + }; + expectTypeOf>().toEqualTypeOf(); + }); + + test('bidirectional assignability for concrete types', () => { + type Original = { a: number } & { b: string; nested: { c: boolean } & { d: number } }; + type Simplified = SimplifyDeep; + + expectTypeOf().toExtend(); + expectTypeOf().toExtend(); + }); +}); + +describe('Collection result types are simplified', () => { + const runtime = createMockRuntime(); + const context = getTestContext(); + + test('default Row is a plain object', () => { + const users = new Collection({ runtime, context }, 'User'); + type UserRow = Awaited>; + expectTypeOf>().toEqualTypeOf<{ + id: number; + name: string; + email: string; + invitedById: number | null; + address: { + readonly street: string; + readonly city: string; + readonly zip: string | null; + } | null; + }>(); + }); + + test('select() produces a plain object', () => { + const users = new Collection({ runtime, context }, 'User'); + const selected = users.select('id', 'email'); + type SelectedRow = Awaited>; + expectTypeOf>().toEqualTypeOf<{ + id: number; + email: string; + }>(); + }); + + test('include() produces a plain object with nested relation', () => { + const users = new Collection({ runtime, context }, 'User'); + const withPosts = users.include('posts'); + type WithPostsRow = Awaited>; + expectTypeOf>().toEqualTypeOf<{ + id: number; + name: string; + email: string; + invitedById: number | null; + address: { + readonly street: string; + readonly city: string; + readonly zip: string | null; + } | null; + posts: { + id: number; + title: string; + userId: number; + views: number; + embedding: number[] | null; + }[]; + }>(); + }); + + test('select().include() produces a plain object', () => { + const users = new Collection({ runtime, context }, 'User'); + const selected = users.select('name').include('posts'); + type Row = Awaited>; + expectTypeOf>().toEqualTypeOf<{ + name: string; + posts: { + id: number; + title: string; + userId: number; + views: number; + embedding: number[] | null; + }[]; + }>(); + }); + + test('include() with non-nullable to-one relation', () => { + const posts = new Collection({ runtime, context }, 'Post'); + const withAuthor = posts.include('author'); + type Row = Awaited>; + type AuthorField = NonNullable['author']; + expectTypeOf().toEqualTypeOf<{ + id: number; + name: string; + email: string; + invitedById: number | null; + address: { + readonly street: string; + readonly city: string; + readonly zip: string | null; + } | null; + }>(); + }); + + test('include() with count refinement', () => { + const users = new Collection({ runtime, context }, 'User'); + const withPostCount = users.include('posts', (posts) => posts.count()); + type Row = Awaited>; + expectTypeOf['posts']>().toEqualTypeOf(); + }); +}); From 55cb9a913221772d748485b0a2c2f75e30633419 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Wed, 15 Apr 2026 22:44:16 +0300 Subject: [PATCH 2/4] refactor: unify SimplifyDeep types and move them to the utils package --- examples/mongo-demo/package.json | 1 + examples/mongo-demo/src/server.ts | 2 +- .../0-foundation/utils/package.json | 1 + .../utils/src/exports/simplify-deep.ts | 1 + .../0-foundation/utils/src/simplify-deep.ts | 16 ++++ .../utils/test/simplify-deep.test.ts | 74 +++++++++++++++++++ .../0-foundation/utils/tsdown.config.ts | 1 + .../5-query-builders/orm/package.json | 5 +- .../5-query-builders/orm/src/exports/index.ts | 2 +- .../5-query-builders/orm/src/types.ts | 10 --- .../sql-orm-client/src/collection.ts | 2 +- .../sql-orm-client/src/grouped-collection.ts | 2 +- .../3-extensions/sql-orm-client/src/types.ts | 12 --- .../sql-orm-client/test/simplify-deep.test.ts | 67 ----------------- pnpm-lock.yaml | 6 ++ 15 files changed, 107 insertions(+), 95 deletions(-) create mode 100644 packages/1-framework/0-foundation/utils/src/exports/simplify-deep.ts create mode 100644 packages/1-framework/0-foundation/utils/src/simplify-deep.ts create mode 100644 packages/1-framework/0-foundation/utils/test/simplify-deep.test.ts diff --git a/examples/mongo-demo/package.json b/examples/mongo-demo/package.json index 2141bf20ea..e3e2852b4b 100644 --- a/examples/mongo-demo/package.json +++ b/examples/mongo-demo/package.json @@ -25,6 +25,7 @@ "@prisma-next/mongo-pipeline-builder": "workspace:*", "@prisma-next/mongo-query-ast": "workspace:*", "@prisma-next/mongo-runtime": "workspace:*", + "@prisma-next/utils": "workspace:*", "mongodb": "catalog:", "react": "^19.2.4", "react-dom": "^19.2.4" diff --git a/examples/mongo-demo/src/server.ts b/examples/mongo-demo/src/server.ts index cafe261c63..4764a50b71 100644 --- a/examples/mongo-demo/src/server.ts +++ b/examples/mongo-demo/src/server.ts @@ -1,6 +1,6 @@ import { existsSync } from 'node:fs'; import { createServer, type ServerResponse } from 'node:http'; -import type { SimplifyDeep } from '@prisma-next/mongo-orm'; +import type { SimplifyDeep } from '@prisma-next/utils/simplify-deep'; if (existsSync('.env')) { process.loadEnvFile('.env'); diff --git a/packages/1-framework/0-foundation/utils/package.json b/packages/1-framework/0-foundation/utils/package.json index 835914e944..80198028e8 100644 --- a/packages/1-framework/0-foundation/utils/package.json +++ b/packages/1-framework/0-foundation/utils/package.json @@ -33,6 +33,7 @@ "./defined": "./dist/defined.mjs", "./redact-db-url": "./dist/redact-db-url.mjs", "./result": "./dist/result.mjs", + "./simplify-deep": "./dist/simplify-deep.mjs", "./package.json": "./package.json" }, "repository": { diff --git a/packages/1-framework/0-foundation/utils/src/exports/simplify-deep.ts b/packages/1-framework/0-foundation/utils/src/exports/simplify-deep.ts new file mode 100644 index 0000000000..7e2a030359 --- /dev/null +++ b/packages/1-framework/0-foundation/utils/src/exports/simplify-deep.ts @@ -0,0 +1 @@ +export type { SimplifyDeep } from '../simplify-deep'; diff --git a/packages/1-framework/0-foundation/utils/src/simplify-deep.ts b/packages/1-framework/0-foundation/utils/src/simplify-deep.ts new file mode 100644 index 0000000000..4fe6b5c897 --- /dev/null +++ b/packages/1-framework/0-foundation/utils/src/simplify-deep.ts @@ -0,0 +1,16 @@ +export type SimplifyDeep = T extends readonly (infer Element)[] + ? SimplifyDeep[] + : T extends + | string + | number + | boolean + | bigint + | symbol + | Date + | RegExp + | Uint8Array + | ((...args: never[]) => unknown) + ? T + : T extends object + ? { [K in keyof T]: SimplifyDeep } + : T; diff --git a/packages/1-framework/0-foundation/utils/test/simplify-deep.test.ts b/packages/1-framework/0-foundation/utils/test/simplify-deep.test.ts new file mode 100644 index 0000000000..7ee0867fae --- /dev/null +++ b/packages/1-framework/0-foundation/utils/test/simplify-deep.test.ts @@ -0,0 +1,74 @@ +import { describe, expectTypeOf, test } from 'vitest'; +import type { SimplifyDeep } from '../src/simplify-deep'; + +describe('SimplifyDeep', () => { + test('primitives pass through', () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test('branded primitives pass through', () => { + type Branded = string & { readonly __brand: true }; + expectTypeOf>().toEqualTypeOf(); + }); + + test('Date, RegExp, and Uint8Array preserved', () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test('functions preserved', () => { + type Fn = (a: number, b: string) => boolean; + expectTypeOf>().toEqualTypeOf(); + }); + + test('intersections flatten into plain objects', () => { + type Input = { a: number } & { b: string }; + type Expected = { a: number; b: string }; + expectTypeOf>().toEqualTypeOf(); + }); + + test('arrays recurse', () => { + type Input = ({ a: number } & { b: string })[]; + type Expected = { a: number; b: string }[]; + expectTypeOf>().toEqualTypeOf(); + }); + + test('nested objects recurse', () => { + type Input = { nested: { a: number } & { b: string } }; + type Expected = { nested: { a: number; b: string } }; + expectTypeOf>().toEqualTypeOf(); + }); + + test('nullable objects', () => { + type Input = ({ a: number } & { b: string }) | null; + type Expected = { a: number; b: string } | null; + expectTypeOf>().toEqualTypeOf(); + }); + + test('nested arrays of intersected objects', () => { + type Input = { + items: ({ id: number } & { name: string })[]; + }; + type Expected = { + items: { id: number; name: string }[]; + }; + expectTypeOf>().toEqualTypeOf(); + }); + + test('bidirectional assignability for concrete types', () => { + type Original = { a: number } & { b: string; nested: { c: boolean } & { d: number } }; + type Simplified = SimplifyDeep; + + expectTypeOf().toExtend(); + expectTypeOf().toExtend(); + }); +}); diff --git a/packages/1-framework/0-foundation/utils/tsdown.config.ts b/packages/1-framework/0-foundation/utils/tsdown.config.ts index 141a29e845..d5f8138889 100644 --- a/packages/1-framework/0-foundation/utils/tsdown.config.ts +++ b/packages/1-framework/0-foundation/utils/tsdown.config.ts @@ -8,5 +8,6 @@ export default defineConfig({ 'src/exports/defined.ts', 'src/exports/result.ts', 'src/exports/redact-db-url.ts', + 'src/exports/simplify-deep.ts', ], }); diff --git a/packages/2-mongo-family/5-query-builders/orm/package.json b/packages/2-mongo-family/5-query-builders/orm/package.json index 6d0eb76f7d..0366587cfe 100644 --- a/packages/2-mongo-family/5-query-builders/orm/package.json +++ b/packages/2-mongo-family/5-query-builders/orm/package.json @@ -16,10 +16,11 @@ }, "dependencies": { "@prisma-next/contract": "workspace:*", + "@prisma-next/framework-components": "workspace:*", "@prisma-next/mongo-contract": "workspace:*", "@prisma-next/mongo-query-ast": "workspace:*", - "@prisma-next/framework-components": "workspace:*", - "@prisma-next/mongo-value": "workspace:*" + "@prisma-next/mongo-value": "workspace:*", + "@prisma-next/utils": "workspace:*" }, "devDependencies": { "@prisma-next/adapter-mongo": "workspace:*", diff --git a/packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts b/packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts index e4d287db32..38b120f569 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts @@ -1,3 +1,4 @@ +export type { SimplifyDeep } from '@prisma-next/utils/simplify-deep'; export type { MongoCollection } from '../collection'; export { createMongoCollection } from '../collection'; export { compileMongoQuery } from '../compile'; @@ -27,7 +28,6 @@ export type { MongoWhereFilter, NoIncludes, ResolvedCreateInput, - SimplifyDeep, VariantCreateInput, VariantModelRow, VariantNames, diff --git a/packages/2-mongo-family/5-query-builders/orm/src/types.ts b/packages/2-mongo-family/5-query-builders/orm/src/types.ts index 95e360dde2..692d9299dd 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/types.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/types.ts @@ -11,16 +11,6 @@ import type { type Simplify = T extends unknown ? { [K in keyof T]: T[K] } : never; -export type SimplifyDeep = T extends readonly (infer E)[] - ? SimplifyDeep[] - : T extends Date | RegExp | ((...args: never[]) => unknown) - ? T - : T extends object - ? T extends unknown - ? { [K in keyof T]: SimplifyDeep } - : never - : T; - type ModelRelations< TContract extends MongoContract, ModelName extends string & keyof TContract['models'], diff --git a/packages/3-extensions/sql-orm-client/src/collection.ts b/packages/3-extensions/sql-orm-client/src/collection.ts index d1a334cbb5..3173e4c000 100644 --- a/packages/3-extensions/sql-orm-client/src/collection.ts +++ b/packages/3-extensions/sql-orm-client/src/collection.ts @@ -10,6 +10,7 @@ import { type ToWhereExpr, type WhereArg, } from '@prisma-next/sql-relational-core/ast'; +import type { SimplifyDeep } from '@prisma-next/utils/simplify-deep'; import { createAggregateBuilder, isAggregateSelector } from './aggregate-builder'; import { normalizeAggregateResult } from './collection-aggregate-result'; import { mapCursorValuesToColumns, mapFieldsToColumns } from './collection-column-mapping'; @@ -107,7 +108,6 @@ import { type RelationNames, type ResolvedCreateInput, type ShorthandWhereFilter, - type SimplifyDeep, type UniqueConstraintCriterion, type VariantModelRow, type VariantNames, diff --git a/packages/3-extensions/sql-orm-client/src/grouped-collection.ts b/packages/3-extensions/sql-orm-client/src/grouped-collection.ts index 89df648d2f..d594ad6e8c 100644 --- a/packages/3-extensions/sql-orm-client/src/grouped-collection.ts +++ b/packages/3-extensions/sql-orm-client/src/grouped-collection.ts @@ -8,6 +8,7 @@ import { ColumnRef, LiteralExpr, } from '@prisma-next/sql-relational-core/ast'; +import type { SimplifyDeep } from '@prisma-next/utils/simplify-deep'; import { createAggregateBuilder, isAggregateSelector } from './aggregate-builder'; import { getFieldToColumnMap } from './collection-contract'; import { mapStorageRowToModelFields } from './collection-runtime'; @@ -21,7 +22,6 @@ import type { DefaultModelRow, HavingBuilder, HavingComparisonMethods, - SimplifyDeep, } from './types'; import { combineWhereExprs } from './where-utils'; diff --git a/packages/3-extensions/sql-orm-client/src/types.ts b/packages/3-extensions/sql-orm-client/src/types.ts index 46122bf82a..3a76d01700 100644 --- a/packages/3-extensions/sql-orm-client/src/types.ts +++ b/packages/3-extensions/sql-orm-client/src/types.ts @@ -22,18 +22,6 @@ import type { ExecutionContext } from '@prisma-next/sql-relational-core/query-la import type { ComputeColumnJsType } from '@prisma-next/sql-relational-core/types'; import type { RowSelection } from './collection-internal-types'; -// --------------------------------------------------------------------------- -// SimplifyDeep — recursive type prettifier for IDE tooltips -// --------------------------------------------------------------------------- - -export type SimplifyDeep = T extends readonly (infer Element)[] - ? SimplifyDeep[] - : T extends string | number | boolean | bigint | symbol | Date | Uint8Array - ? T - : T extends object - ? { [K in keyof T]: SimplifyDeep } - : T; - // --------------------------------------------------------------------------- // Comparison / Filter / Order / Include // --------------------------------------------------------------------------- diff --git a/packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts b/packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts index c09a5dab18..c95614a75b 100644 --- a/packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts +++ b/packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts @@ -1,74 +1,7 @@ import { describe, expectTypeOf, test } from 'vitest'; import { Collection } from '../src/collection'; -import type { SimplifyDeep } from '../src/types'; import { createMockRuntime, getTestContext } from './helpers'; -describe('SimplifyDeep', () => { - test('primitives pass through', () => { - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - }); - - test('branded primitives pass through', () => { - type Branded = string & { readonly __brand: true }; - expectTypeOf>().toEqualTypeOf(); - }); - - test('Date and Uint8Array preserved', () => { - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - }); - - test('intersections flatten into plain objects', () => { - type Input = { a: number } & { b: string }; - type Expected = { a: number; b: string }; - expectTypeOf>().toEqualTypeOf(); - }); - - test('arrays recurse', () => { - type Input = ({ a: number } & { b: string })[]; - type Expected = { a: number; b: string }[]; - expectTypeOf>().toEqualTypeOf(); - }); - - test('nested objects recurse', () => { - type Input = { nested: { a: number } & { b: string } }; - type Expected = { nested: { a: number; b: string } }; - expectTypeOf>().toEqualTypeOf(); - }); - - test('nullable objects', () => { - type Input = ({ a: number } & { b: string }) | null; - type Expected = { a: number; b: string } | null; - expectTypeOf>().toEqualTypeOf(); - }); - - test('nested arrays of intersected objects', () => { - type Input = { - items: ({ id: number } & { name: string })[]; - }; - type Expected = { - items: { id: number; name: string }[]; - }; - expectTypeOf>().toEqualTypeOf(); - }); - - test('bidirectional assignability for concrete types', () => { - type Original = { a: number } & { b: string; nested: { c: boolean } & { d: number } }; - type Simplified = SimplifyDeep; - - expectTypeOf().toExtend(); - expectTypeOf().toExtend(); - }); -}); - describe('Collection result types are simplified', () => { const runtime = createMockRuntime(); const context = getTestContext(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd6e78b8b4..6ff5005dd0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,9 @@ importers: '@prisma-next/mongo-runtime': specifier: workspace:* version: link:../../packages/2-mongo-family/7-runtime + '@prisma-next/utils': + specifier: workspace:* + version: link:../../packages/1-framework/0-foundation/utils mongodb: specifier: 'catalog:' version: 6.21.0 @@ -1192,6 +1195,9 @@ importers: '@prisma-next/mongo-value': specifier: workspace:* version: link:../../1-foundation/mongo-value + '@prisma-next/utils': + specifier: workspace:* + version: link:../../../1-framework/0-foundation/utils devDependencies: '@prisma-next/adapter-mongo': specifier: workspace:* From f8ff24a27b03bd4fb36fa8d5f57fcfa955c5dea7 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Wed, 15 Apr 2026 23:57:22 +0300 Subject: [PATCH 3/4] fix: address review comment F03 --- .../1-framework/0-foundation/utils/src/simplify-deep.ts | 4 +++- .../{simplify-deep.test.ts => simplify-deep.test-d.ts} | 8 +++++++- packages/1-framework/0-foundation/utils/vitest.config.ts | 4 ++++ 3 files changed, 14 insertions(+), 2 deletions(-) rename packages/1-framework/0-foundation/utils/test/{simplify-deep.test.ts => simplify-deep.test-d.ts} (90%) diff --git a/packages/1-framework/0-foundation/utils/src/simplify-deep.ts b/packages/1-framework/0-foundation/utils/src/simplify-deep.ts index 4fe6b5c897..ad1c84283f 100644 --- a/packages/1-framework/0-foundation/utils/src/simplify-deep.ts +++ b/packages/1-framework/0-foundation/utils/src/simplify-deep.ts @@ -1,5 +1,7 @@ export type SimplifyDeep = T extends readonly (infer Element)[] - ? SimplifyDeep[] + ? T extends unknown[] + ? SimplifyDeep[] + : readonly SimplifyDeep[] : T extends | string | number diff --git a/packages/1-framework/0-foundation/utils/test/simplify-deep.test.ts b/packages/1-framework/0-foundation/utils/test/simplify-deep.test-d.ts similarity index 90% rename from packages/1-framework/0-foundation/utils/test/simplify-deep.test.ts rename to packages/1-framework/0-foundation/utils/test/simplify-deep.test-d.ts index 7ee0867fae..0ad91beadc 100644 --- a/packages/1-framework/0-foundation/utils/test/simplify-deep.test.ts +++ b/packages/1-framework/0-foundation/utils/test/simplify-deep.test-d.ts @@ -36,12 +36,18 @@ describe('SimplifyDeep', () => { expectTypeOf>().toEqualTypeOf(); }); - test('arrays recurse', () => { + test('mutable arrays recurse', () => { type Input = ({ a: number } & { b: string })[]; type Expected = { a: number; b: string }[]; expectTypeOf>().toEqualTypeOf(); }); + test('readonly arrays preserve readonly', () => { + type Input = readonly ({ a: number } & { b: string })[]; + type Expected = readonly { a: number; b: string }[]; + expectTypeOf>().toEqualTypeOf(); + }); + test('nested objects recurse', () => { type Input = { nested: { a: number } & { b: string } }; type Expected = { nested: { a: number; b: string } }; diff --git a/packages/1-framework/0-foundation/utils/vitest.config.ts b/packages/1-framework/0-foundation/utils/vitest.config.ts index cb29ab9f30..9392f4a6c6 100644 --- a/packages/1-framework/0-foundation/utils/vitest.config.ts +++ b/packages/1-framework/0-foundation/utils/vitest.config.ts @@ -5,6 +5,10 @@ export default defineConfig({ globals: true, environment: 'node', include: ['test/**/*.test.ts'], + typecheck: { + enabled: true, + include: ['test/**/*.test-d.ts'], + }, coverage: { provider: 'v8', reporter: ['text', 'lcov'], From 10de666ca7f854dc1922f6b5d4aa892359673ab6 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Thu, 16 Apr 2026 00:00:34 +0300 Subject: [PATCH 4/4] test: address review comment F02 --- ...y-deep.test.ts => simplify-deep.test-d.ts} | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) rename packages/3-extensions/sql-orm-client/test/{simplify-deep.test.ts => simplify-deep.test-d.ts} (76%) diff --git a/packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts b/packages/3-extensions/sql-orm-client/test/simplify-deep.test-d.ts similarity index 76% rename from packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts rename to packages/3-extensions/sql-orm-client/test/simplify-deep.test-d.ts index c95614a75b..b3ea919d0e 100644 --- a/packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts +++ b/packages/3-extensions/sql-orm-client/test/simplify-deep.test-d.ts @@ -90,6 +90,41 @@ describe('Collection result types are simplified', () => { }>(); }); + test('chained include() produces a plain object', () => { + const users = new Collection({ runtime, context }, 'User'); + const withPostsAndInviter = users.include('posts').include('invitedBy'); + type Row = Awaited>; + expectTypeOf>().toEqualTypeOf<{ + id: number; + name: string; + email: string; + invitedById: number | null; + address: { + readonly street: string; + readonly city: string; + readonly zip: string | null; + } | null; + posts: { + id: number; + title: string; + userId: number; + views: number; + embedding: number[] | null; + }[]; + invitedBy: { + id: number; + name: string; + email: string; + invitedById: number | null; + address: { + readonly street: string; + readonly city: string; + readonly zip: string | null; + } | null; + } | null; + }>(); + }); + test('include() with count refinement', () => { const users = new Collection({ runtime, context }, 'User'); const withPostCount = users.include('posts', (posts) => posts.count());