-
Notifications
You must be signed in to change notification settings - Fork 3
feat(sql-orm-client): expand and simplify output types #283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,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 | ||
| // --------------------------------------------------------------------------- | ||
|
Comment on lines
+24
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. F04 — Non-blocking:
Not blocking this PR, but should be done as a follow-up before other packages duplicate the type. |
||
|
|
||
| export type SimplifyDeep<T> = T extends readonly (infer Element)[] | ||
| ? SimplifyDeep<Element>[] | ||
|
Comment on lines
+28
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. F03 — Non-blocking: The array branch matches Suggestion: Either preserve test('readonly arrays become mutable (intentional)', () => {
type Input = readonly ({ a: number } & { b: string })[];
type Result = SimplifyDeep<Input>;
expectTypeOf<Result>().toEqualTypeOf<{ a: number; b: string }[]>();
}); |
||
| : T extends string | number | boolean | bigint | symbol | Date | Uint8Array | ||
| ? T | ||
| : T extends object | ||
| ? { [K in keyof T]: SimplifyDeep<T[K]> } | ||
| : T; | ||
|
Comment on lines
+28
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. F05 — Non-blocking: Forward risk — polymorphic model compatibility Polymorphic model support (ADR 173 — discriminator + variants) will be ported to the SQL domain and will impact this ORM client. Experience implementing a similar helper in the Mongo domain surfaced issues with polymorphic model type shapes.
Not a blocker — just flagging that when polymorphic models land in SQL ORM, this helper will need verification and may need extension or removal. |
||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Comparison / Filter / Order / Include | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| 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<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>(); | ||
| }); | ||
|
|
||
| test('branded primitives pass through', () => { | ||
| type Branded = string & { readonly __brand: true }; | ||
| expectTypeOf<SimplifyDeep<Branded>>().toEqualTypeOf<Branded>(); | ||
| }); | ||
|
|
||
| test('Date and Uint8Array preserved', () => { | ||
| expectTypeOf<SimplifyDeep<Date>>().toEqualTypeOf<Date>(); | ||
| expectTypeOf<SimplifyDeep<Uint8Array>>().toEqualTypeOf<Uint8Array>(); | ||
| }); | ||
|
|
||
| test('intersections flatten into plain objects', () => { | ||
| type Input = { a: number } & { b: string }; | ||
| type Expected = { a: number; b: string }; | ||
| expectTypeOf<SimplifyDeep<Input>>().toEqualTypeOf<Expected>(); | ||
| }); | ||
|
|
||
| test('arrays recurse', () => { | ||
| type Input = ({ a: number } & { b: string })[]; | ||
| type Expected = { 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>(); | ||
| }); | ||
| }); | ||
|
|
||
| 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<ReturnType<typeof users.first>>; | ||
| expectTypeOf<NonNullable<UserRow>>().toEqualTypeOf<{ | ||
| id: number; | ||
| name: string; | ||
| email: string; | ||
| invitedById: number | null; | ||
| }>(); | ||
| }); | ||
|
|
||
| test('select() produces a plain object', () => { | ||
| const users = new Collection({ runtime, context }, 'User'); | ||
| const selected = users.select('id', 'email'); | ||
| type SelectedRow = Awaited<ReturnType<typeof selected.first>>; | ||
| expectTypeOf<NonNullable<SelectedRow>>().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<ReturnType<typeof withPosts.first>>; | ||
| expectTypeOf<NonNullable<WithPostsRow>>().toEqualTypeOf<{ | ||
| id: number; | ||
| name: string; | ||
| email: string; | ||
| invitedById: number | null; | ||
| posts: { | ||
| id: number; | ||
| title: string; | ||
| userId: number; | ||
| views: number; | ||
| }[]; | ||
| }>(); | ||
| }); | ||
|
|
||
| test('select().include() produces a plain object', () => { | ||
| const users = new Collection({ runtime, context }, 'User'); | ||
| const selected = users.select('name').include('posts'); | ||
| type Row = Awaited<ReturnType<typeof selected.first>>; | ||
| expectTypeOf<NonNullable<Row>>().toEqualTypeOf<{ | ||
| name: string; | ||
| posts: { | ||
| id: number; | ||
| title: string; | ||
| userId: number; | ||
| views: number; | ||
| }[]; | ||
| }>(); | ||
| }); | ||
|
|
||
| test('include() with non-nullable to-one relation', () => { | ||
| const posts = new Collection({ runtime, context }, 'Post'); | ||
| const withAuthor = posts.include('author'); | ||
| type Row = Awaited<ReturnType<typeof withAuthor.first>>; | ||
| type AuthorField = NonNullable<Row>['author']; | ||
| expectTypeOf<AuthorField>().toEqualTypeOf<{ | ||
| id: number; | ||
| name: string; | ||
| email: string; | ||
| invitedById: number | null; | ||
| }>(); | ||
| }); | ||
|
|
||
| test('include() with count refinement', () => { | ||
| const users = new Collection({ runtime, context }, 'User'); | ||
| const withPostCount = users.include('posts', (posts) => posts.count()); | ||
| type Row = Awaited<ReturnType<typeof withPostCount.first>>; | ||
| expectTypeOf<NonNullable<Row>['posts']>().toEqualTypeOf<number>(); | ||
| }); | ||
| }); | ||
|
Comment on lines
+72
to
+149
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. F02 — Blocking: No test coverage for multi-step chaining (the primary use case) Deeply nested chains are the primary use case that motivated this change — they produce the most unreadable intersections in IDE tooltips. The tests here verify individual operations and one two-method chain ( Since each chaining step currently wraps the previous Suggestion: Add at least one multi-include chain test: test('chained include() produces a plain object', () => {
const users = new Collection({ runtime, context }, 'User');
const withPostsAndInviter = users.include('posts').include('invitedBy');
type Row = Awaited<ReturnType<typeof withPostsAndInviter.first>>;
expectTypeOf<NonNullable<Row>>().toEqualTypeOf<{
id: number;
name: string;
email: string;
invitedById: number | null;
posts: { id: number; title: string; userId: number; views: number }[];
invitedBy: { id: number; name: string; email: string; invitedById: number | null } | null;
}>();
}); |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
F01 — Blocking:
SimplifyDeepapplied at every chaining step instead of terminal methodsThe default
Rowis alreadySimplifyDeep<DefaultModelRow<...>>. Wheninclude()returnsSimplifyDeep<Row & { posts: ... }>, this becomesSimplifyDeep<SimplifyDeep<...> & { ... }>. Each chaining step adds another wrapper. Forusers.select('name').include('posts'), that's three nestedSimplifyDeepevaluations — O(N×K) work for N chained ops with K total keys.Suggestion: Apply
SimplifyDeeponly 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.