Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/mongo-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion examples/mongo-demo/src/server.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
1 change: 1 addition & 0 deletions packages/1-framework/0-foundation/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { SimplifyDeep } from '../simplify-deep';
18 changes: 18 additions & 0 deletions packages/1-framework/0-foundation/utils/src/simplify-deep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type SimplifyDeep<T> = T extends readonly (infer Element)[]
? T extends unknown[]
? SimplifyDeep<Element>[]
: readonly SimplifyDeep<Element>[]
: T extends
| string
| number
| boolean
| bigint
| symbol
| Date
| RegExp
| Uint8Array
| ((...args: never[]) => unknown)
? T
: T extends object
? { [K in keyof T]: SimplifyDeep<T[K]> }
: T;
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expectTypeOf, test } from 'vitest';
import type { SimplifyDeep } from '../src/simplify-deep';

describe('SimplifyDeep', () => {
test('primitives pass through', () => {
expectTypeOf<SimplifyDeep<string>>().toEqualTypeOf<string>();
expectTypeOf<SimplifyDeep<number>>().toEqualTypeOf<number>();
expectTypeOf<SimplifyDeep<boolean>>().toEqualTypeOf<boolean>();
expectTypeOf<SimplifyDeep<bigint>>().toEqualTypeOf<bigint>();
expectTypeOf<SimplifyDeep<symbol>>().toEqualTypeOf<symbol>();
expectTypeOf<SimplifyDeep<null>>().toEqualTypeOf<null>();
expectTypeOf<SimplifyDeep<undefined>>().toEqualTypeOf<undefined>();
expectTypeOf<SimplifyDeep<unknown>>().toEqualTypeOf<unknown>();
expectTypeOf<SimplifyDeep<never>>().toEqualTypeOf<never>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify existing never-assertion patterns used across declaration tests.
rg -nP --type=ts 'toEqualTypeOf<never>\(\)' -g '**/*.test-d.ts'
rg -nP --type=ts 'toEqualTypeOf\(undefined as never\)' -g '**/*.test-d.ts'

Repository: prisma/prisma-next

Length of output: 604


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if type test files are included in test configuration
if [ -f "vitest.config.ts" ] || [ -f "vitest.config.mts" ]; then
  echo "=== Vitest Configuration ===" 
  head -50 vitest.config.* 2>/dev/null | grep -A5 -B5 "test-d" || echo "No test-d pattern found in config start"
fi

# Count all instances of the problematic pattern
echo "=== Pattern: toEqualTypeOf<never>() instances ==="
rg -c 'toEqualTypeOf<never>\(\)' -g '**/*.test-d.ts' || echo "0"

# List files that need fixing
echo "=== Files using problematic pattern ==="
rg -l 'toEqualTypeOf<never>\(\)' -g '**/*.test-d.ts'

# Check for any type test errors in the build
if [ -f "pnpm-lock.yaml" ]; then
  echo "=== Package manager: pnpm detected ==="
fi

Repository: prisma/prisma-next

Length of output: 474


Use the documented workaround pattern for never type assertions.

Line 14 uses toEqualTypeOf<never>(), which does not compile with the current Vitest version. Replace with the value-form workaround as already used in the integration test suite.

Proposed fix
-    expectTypeOf<SimplifyDeep<never>>().toEqualTypeOf<never>();
+    expectTypeOf<SimplifyDeep<never>>().toEqualTypeOf(undefined as never);

Note: The same issue exists in packages/2-mongo-family/1-foundation/mongo-codec/test/codecs.test-d.ts:51 and should be fixed with the same pattern.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expectTypeOf<SimplifyDeep<never>>().toEqualTypeOf<never>();
expectTypeOf<SimplifyDeep<never>>().toEqualTypeOf(undefined as never);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/0-foundation/utils/test/simplify-deep.test-d.ts` at line
14, The test uses the type-form assertion
expectTypeOf<SimplifyDeep<never>>().toEqualTypeOf<never>() which fails with the
current Vitest; replace it with the documented value-form workaround used
elsewhere: create a value cast to SimplifyDeep<never> (e.g. use a plain value
cast like undefined as SimplifyDeep<never>) and call expectTypeOf on that value,
then assert equality to never via toEqualTypeOf; apply the same change pattern
where you see the same failing assertion (look for
expectTypeOf<...>().toEqualTypeOf<never> and SimplifyDeep usages).

});

test('branded primitives pass through', () => {
type Branded = string & { readonly __brand: true };
expectTypeOf<SimplifyDeep<Branded>>().toEqualTypeOf<Branded>();
});

test('Date, RegExp, and Uint8Array preserved', () => {
expectTypeOf<SimplifyDeep<Date>>().toEqualTypeOf<Date>();
expectTypeOf<SimplifyDeep<RegExp>>().toEqualTypeOf<RegExp>();
expectTypeOf<SimplifyDeep<Uint8Array>>().toEqualTypeOf<Uint8Array>();
});

test('functions preserved', () => {
type Fn = (a: number, b: string) => boolean;
expectTypeOf<SimplifyDeep<Fn>>().toEqualTypeOf<Fn>();
});

test('intersections flatten into plain objects', () => {
type Input = { a: number } & { b: string };
type Expected = { a: number; b: string };
expectTypeOf<SimplifyDeep<Input>>().toEqualTypeOf<Expected>();
});

test('mutable arrays recurse', () => {
type Input = ({ a: number } & { b: string })[];
type Expected = { a: number; b: string }[];
expectTypeOf<SimplifyDeep<Input>>().toEqualTypeOf<Expected>();
});

test('readonly arrays preserve readonly', () => {
type Input = readonly ({ a: number } & { b: string })[];
type Expected = readonly { a: number; b: string }[];
expectTypeOf<SimplifyDeep<Input>>().toEqualTypeOf<Expected>();
});

test('nested objects recurse', () => {
type Input = { nested: { a: number } & { b: string } };
type Expected = { nested: { a: number; b: string } };
expectTypeOf<SimplifyDeep<Input>>().toEqualTypeOf<Expected>();
});

test('nullable objects', () => {
type Input = ({ a: number } & { b: string }) | null;
type Expected = { a: number; b: string } | null;
expectTypeOf<SimplifyDeep<Input>>().toEqualTypeOf<Expected>();
});

test('nested arrays of intersected objects', () => {
type Input = {
items: ({ id: number } & { name: string })[];
};
type Expected = {
items: { id: number; name: string }[];
};
expectTypeOf<SimplifyDeep<Input>>().toEqualTypeOf<Expected>();
});

test('bidirectional assignability for concrete types', () => {
type Original = { a: number } & { b: string; nested: { c: boolean } & { d: number } };
type Simplified = SimplifyDeep<Original>;

expectTypeOf<Original>().toExtend<Simplified>();
expectTypeOf<Simplified>().toExtend<Original>();
});
});
1 change: 1 addition & 0 deletions packages/1-framework/0-foundation/utils/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
});
4 changes: 4 additions & 0 deletions packages/1-framework/0-foundation/utils/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
5 changes: 3 additions & 2 deletions packages/2-mongo-family/5-query-builders/orm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -27,7 +28,6 @@ export type {
MongoWhereFilter,
NoIncludes,
ResolvedCreateInput,
SimplifyDeep,
VariantCreateInput,
VariantModelRow,
VariantNames,
Expand Down
10 changes: 0 additions & 10 deletions packages/2-mongo-family/5-query-builders/orm/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,6 @@ import type {

type Simplify<T> = T extends unknown ? { [K in keyof T]: T[K] } : never;

export type SimplifyDeep<T> = T extends readonly (infer E)[]
? SimplifyDeep<E>[]
: T extends Date | RegExp | ((...args: never[]) => unknown)
? T
: T extends object
? T extends unknown
? { [K in keyof T]: SimplifyDeep<T[K]> }
: never
: T;

type ModelRelations<
TContract extends MongoContract,
ModelName extends string & keyof TContract['models'],
Expand Down
55 changes: 32 additions & 23 deletions packages/3-extensions/sql-orm-client/src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -159,7 +160,7 @@ interface MtiCreateContext {
export class Collection<
TContract extends Contract<SqlStorage>,
ModelName extends string,
Row = InferRootRow<TContract, ModelName>,
Row = SimplifyDeep<InferRootRow<TContract, ModelName>>,
State extends CollectionTypeState = DefaultCollectionTypeState,
Comment on lines 160 to 164
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F01 — Blocking: SimplifyDeep applied at every chaining step instead of terminal methods

The default Row is already SimplifyDeep<DefaultModelRow<...>>. When include() returns SimplifyDeep<Row & { posts: ... }>, this becomes SimplifyDeep<SimplifyDeep<...> & { ... }>. Each chaining step adds another wrapper. For users.select('name').include('posts'), that's three nested SimplifyDeep evaluations — O(N×K) work for N chained ops with K total keys.

Suggestion: Apply SimplifyDeep only at the terminal methods (first(), all(), toArray()) instead of at each builder step. Builder methods accumulate raw intersections internally; only the final result type gets simplified. This reduces overhead to exactly one pass regardless of chain length and is architecturally cleaner — simplification is a presentation concern, not a builder concern.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each SimplifyDeep step receives AlreadyFlatObject & { newField: ... } as input — not a deeply nested tree. TypeScript caches the resolution of the inner SimplifyDeep, so step 2 isn't re-traversing step 1's keys. Look at what actually happens:

    SimplifyDeep<
      Row & {

Row is already a resolved flat mapped type from the previous step. The outer SimplifyDeep just flattens one intersection of FlatObj & { posts: ... } — a single pass over the combined keys, not a re-walk of the entire history.

Also, if you defer to terminal methods, then hovering over any intermediate builder step shows raw intersections like { id: number; name: string; ... } & { posts: PostRow[] } & { invitedBy: UserRow | null }.

> implements RowSelection<Row>
{
Expand Down Expand Up @@ -317,15 +318,17 @@ export class Collection<
): Collection<
TContract,
ModelName,
Row & {
[K in RelName]: IncludeRefinementValue<
TContract,
ModelName,
K,
DefaultModelRow<TContract, RelatedName>,
RefinedResult
>;
},
SimplifyDeep<
Row & {
[K in RelName]: IncludeRefinementValue<
TContract,
ModelName,
K,
DefaultModelRow<TContract, RelatedName>,
RefinedResult
>;
}
>,
State
> {
const relation = resolveIncludeRelation(this.contract, this.modelName, relationName as string);
Expand Down Expand Up @@ -391,15 +394,17 @@ export class Collection<
};

return this.#cloneWithRow<
Row & {
[K in RelName]: IncludeRefinementValue<
TContract,
ModelName,
K,
DefaultModelRow<TContract, RelatedName>,
RefinedResult
>;
},
SimplifyDeep<
Row & {
[K in RelName]: IncludeRefinementValue<
TContract,
ModelName,
K,
DefaultModelRow<TContract, RelatedName>,
RefinedResult
>;
}
>,
State
>({
includes: [...this.state.includes, includeExpr],
Expand All @@ -416,15 +421,19 @@ export class Collection<
): Collection<
TContract,
ModelName,
Pick<DefaultModelRow<TContract, ModelName>, Fields[number]> &
IncludedRelationsForRow<TContract, ModelName, Row>,
SimplifyDeep<
Pick<DefaultModelRow<TContract, ModelName>, Fields[number]> &
IncludedRelationsForRow<TContract, ModelName, Row>
>,
State
> {
const selectedFields = mapFieldsToColumns(this.contract, this.modelName, fields);

return this.#cloneWithRow<
Pick<DefaultModelRow<TContract, ModelName>, Fields[number]> &
IncludedRelationsForRow<TContract, ModelName, Row>,
SimplifyDeep<
Pick<DefaultModelRow<TContract, ModelName>, Fields[number]> &
IncludedRelationsForRow<TContract, ModelName, Row>
>,
State
>({
selectedFields,
Expand Down
11 changes: 9 additions & 2 deletions packages/3-extensions/sql-orm-client/src/grouped-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -84,7 +85,11 @@ export class GroupedCollection<
async aggregate<Spec extends AggregateSpec>(
fn: (aggregate: AggregateBuilder<TContract, ModelName>) => Spec,
): Promise<
Array<Pick<DefaultModelRow<TContract, ModelName>, GroupFields[number]> & AggregateResult<Spec>>
Array<
SimplifyDeep<
Pick<DefaultModelRow<TContract, ModelName>, GroupFields[number]> & AggregateResult<Spec>
>
>
> {
const aggregateSpec = fn(createAggregateBuilder(this.contract, this.modelName));
const aggregateEntries = Object.entries(aggregateSpec);
Expand Down Expand Up @@ -118,7 +123,9 @@ export class GroupedCollection<
}
return mapped;
}) as Array<
Pick<DefaultModelRow<TContract, ModelName>, GroupFields[number]> & AggregateResult<Spec>
SimplifyDeep<
Pick<DefaultModelRow<TContract, ModelName>, GroupFields[number]> & AggregateResult<Spec>
>
>;
}
}
Expand Down
Loading
Loading