Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"prettier": "^3.5.3",
"prisma": "catalog:",
"tsdown": "^0.21.8",
"tsx": "^4.20.3",
"tsx": "catalog:",
"turbo": "^2.5.4",
"typescript": "catalog:",
"typescript-eslint": "^8.34.1",
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/actions/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ export async function run(options: Options) {
throw new CliError(`Failed to connect to the database: ${err instanceof Error ? err.message : String(err)}`);
}

startServer(db, schemaModule.schema, options);
// `db` carries a precise options literal (omit, log, etc.); `startServer` only needs a
// schema-agnostic `ClientContract<SchemaDef>`. Those differ in their (invariant) `Options`
// type argument, so widen explicitly - the proxy doesn't rely on option-specific typing.
startServer(db as unknown as ClientContract<SchemaDef>, schemaModule.schema, options);
}

function evaluateUrl(schemaUrl: ConfigExpr) {
Expand Down
2 changes: 1 addition & 1 deletion packages/orm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@
"@zenstackhq/tsdown-config": "workspace:*",
"@zenstackhq/typescript-config": "workspace:*",
"@zenstackhq/vitest-config": "workspace:*",
"tsx": "^4.19.2",
"tsx": "catalog:",
"zod": "^4.1.0"
},
"funding": "https://github.com/sponsors/zenstackhq"
Expand Down
49 changes: 34 additions & 15 deletions packages/orm/src/client/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
TypeDefResult,
} from './crud-types';
import type { Diagnostics } from './diagnostics';
import type { ClientOptions, QueryOptions } from './options';
import type { ClientOptions, QueryOptions, QueryRelevantOptions } from './options';
import type {
ExtClientMembersBase,
ExtQueryArgsBase,
Expand Down Expand Up @@ -63,6 +63,10 @@ export const ExtResultMarker: unique symbol = Symbol('zenstack.client.extResult'

/**
* ZenStack client interface.
*
* Note: this alias resolves to an intersection, so it cannot carry variance annotations itself
* (TS2637). It doesn't need them - measuring its variance recurses into {@link ModelOperations},
* whose annotations short-circuit the expensive cascade. See {@link CommonModelOperations}.
*/
export type ClientContract<
Schema extends SchemaDef,
Expand Down Expand Up @@ -257,7 +261,7 @@ export type ClientContract<
/**
* Factory for creating zod schemas to validate query args.
*/
get $zod(): ZodSchemaFactory<Schema, Options, ExtQueryArgs>;
get $zod(): ZodSchemaFactory<Schema, QueryRelevantOptions<Schema, Options>, ExtQueryArgs>;

/**
* Pushes the schema to the database. For testing purposes only.
Expand All @@ -270,10 +274,13 @@ export type ClientContract<
*/
get $diagnostics(): Promise<Diagnostics>;
} & {
[Key in GetSlicedModels<Schema, Options> as Uncapitalize<Key>]: ModelOperations<
// Project `Options` to its query-relevant subset before fanning out across every model. This
// strips the heavy `computedFields`/`procedures` function types (never read by these types) so
// the 30+ `ModelOperations` instantiations stay cheap. See {@link QueryRelevantOptions}.
[Key in GetSlicedModels<Schema, QueryRelevantOptions<Schema, Options>> as Uncapitalize<Key>]: ModelOperations<
Schema,
Key,
Options,
QueryRelevantOptions<Schema, Options>,
ExtQueryArgs,
ExtResult
>;
Expand Down Expand Up @@ -342,7 +349,7 @@ type SliceOperations<
T extends Record<string, unknown>,
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Options extends ClientOptions<Schema>,
Options extends QueryOptions<Schema>,
> = Omit<
{
// keep only operations included by slicing options
Expand Down Expand Up @@ -419,12 +426,21 @@ export type AllModelOperations<
): ZenStackPromise<CrudReturnType<Schema, Model, 'updateManyAndReturn', T, Options, ExtResult>>;
});

// Explicit variance annotations bypass TypeScript's structural variance *measurement* for this
// large, deeply-recursive generic. Measurement here comes back "unreliable" (the type recurses
// across related models) and is pure wasted work - it dominated type-check time. The annotations
// below match the measured variance and let the checker skip that probing entirely.
type CommonModelOperations<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Options extends QueryOptions<Schema>,
ExtQueryArgs extends ExtQueryArgsBase,
ExtResult extends ExtResultBase<Schema> = {},
in out Schema extends SchemaDef,
in out Model extends GetModels<Schema>,
// `Options` is invariant (it is read for `omit`/`slicing` in both arg and result positions).
// Annotating it keeps the variance-measurement skip. Note: a client built with an explicit
// `omit`/`slicing` literal is then no longer assignable to the bare `ClientContract<Schema>`
// (default options) - schema-agnostic call sites that take `ClientContract<SchemaDef>` should
// accept the client via a cast. This is a rare pattern and worth the type-check speedup.
in out Options extends QueryOptions<Schema>,
in out ExtQueryArgs extends ExtQueryArgsBase,
out ExtResult extends ExtResultBase<Schema> = {},
> = {
/**
* Returns a list of entities.
Expand Down Expand Up @@ -955,12 +971,15 @@ type CommonModelOperations<

export type OperationsRequiringCreate = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert';

// See the note on `CommonModelOperations` - explicit variance annotations skip the expensive,
// "unreliable" variance measurement of this recursive type.
export type ModelOperations<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Options extends ClientOptions<Schema> = ClientOptions<Schema>,
ExtQueryArgs extends ExtQueryArgsBase = {},
ExtResult extends ExtResultBase<Schema> = {},
in out Schema extends SchemaDef,
in out Model extends GetModels<Schema>,
// `Options` is invariant - see the note on {@link CommonModelOperations}.
in out Options extends QueryOptions<Schema> = ClientOptions<Schema>,
in out ExtQueryArgs extends ExtQueryArgsBase = {},
out ExtResult extends ExtResultBase<Schema> = {},
> = SliceOperations<AllModelOperations<Schema, Model, Options, ExtQueryArgs, ExtResult>, Schema, Model, Options>;

//#endregion
Expand Down
38 changes: 21 additions & 17 deletions packages/orm/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1324,9 +1324,9 @@ export type SelectSubset<T, U> = {
: {});

type ToManyRelationFilter<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Field extends RelationFields<Schema, Model>,
in out Schema extends SchemaDef,
in out Model extends GetModels<Schema>,
in out Field extends RelationFields<Schema, Model>,
Options extends QueryOptions<Schema>,
> = {
every?: WhereInput<Schema, RelationFieldType<Schema, Model, Field>, Options>;
Expand Down Expand Up @@ -1453,17 +1453,17 @@ type OppositeRelationAndFK<

//#region Find args

type FilterArgs<Schema extends SchemaDef, Model extends GetModels<Schema>, Options extends QueryOptions<Schema>> = {
type FilterArgs<in out Schema extends SchemaDef, in out Model extends GetModels<Schema>, in out Options extends QueryOptions<Schema>> = {
/**
* Filter conditions
*/
where?: WhereInput<Schema, Model, Options>;
};

type SortAndTakeArgs<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Options extends QueryOptions<Schema>,
in out Schema extends SchemaDef,
in out Model extends GetModels<Schema>,
in out Options extends QueryOptions<Schema>,
> = {
/**
* Number of records to skip
Expand Down Expand Up @@ -1844,9 +1844,9 @@ export type UpdateManyAndReturnArgs<
ExtractExtQueryArgs<ExtQueryArgs, 'updateManyAndReturn'>;

type UpdateManyPayload<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Options extends QueryOptions<Schema> = QueryOptions<Schema>,
in out Schema extends SchemaDef,
in out Model extends GetModels<Schema>,
out Options extends QueryOptions<Schema> = QueryOptions<Schema>,
Without extends string = never,
> = {
/**
Expand Down Expand Up @@ -2105,11 +2105,15 @@ type ToManyRelationUpdateInput<
: 'create' | 'createMany' | 'connectOrCreate' | 'upsert'
>;

// Variance-annotated to skip TypeScript's (unreliable, expensive) variance measurement of this
// recursive arg type. Annotating the parent also short-circuits measurement of its conditional
// children (`DisconnectInput`/`NestedDeleteInput`), which can't be annotated directly. `Options`
// is invariant - see the note on `ClientContract`'s `CommonModelOperations`.
type ToOneRelationUpdateInput<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Field extends RelationFields<Schema, Model>,
Options extends QueryOptions<Schema>,
in out Schema extends SchemaDef,
in out Model extends GetModels<Schema>,
in out Field extends RelationFields<Schema, Model>,
in out Options extends QueryOptions<Schema>,
> = Omit<
{
/**
Expand Down Expand Up @@ -2356,9 +2360,9 @@ type AggCommonOutput<Input> = Input extends true
// #region GroupBy

type GroupByHaving<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Options extends QueryOptions<Schema> = QueryOptions<Schema>,
in out Schema extends SchemaDef,
in out Model extends GetModels<Schema>,
out Options extends QueryOptions<Schema> = QueryOptions<Schema>,
> = Omit<WhereInput<Schema, Model, Options, true, true>, '$expr'>;

export type GroupByArgs<
Expand Down
15 changes: 15 additions & 0 deletions packages/orm/src/client/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@ export type QueryOptions<Schema extends SchemaDef> = {
slicing?: SlicingOptions<Schema>;
};

/**
* Projects a (typically inferred) client options type down to only the members that influence
* ORM typing - the {@link QueryOptions} fields (`omit`, `allowQueryTimeOmitOverride`, `slicing`).
*
* The full options object inferred at `new ZenStackClient(...)` carries heavy function types for
* `computedFields` and `procedures`. Those are never read by the model/operation types, but if the
* raw options type is fanned out across every model's `ModelOperations` instantiation (30+ for a
* typical schema) it inflates type checking dramatically. Projecting to the query-relevant subset
* before the fan-out keeps the per-model types cheap while preserving full options on `$options`.
*/
export type QueryRelevantOptions<Schema extends SchemaDef, Options> = Pick<
Options,
Extract<keyof Options, keyof QueryOptions<Schema>>
>;

/**
* ZenStack client options.
*/
Expand Down
16 changes: 12 additions & 4 deletions packages/orm/src/client/zod/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,11 @@ export type CreateSchemaOptions = {
* Factory class responsible for creating and caching Zod schemas for ORM input validation.
*/
export class ZodSchemaFactory<
Schema extends SchemaDef,
Options extends ClientOptions<Schema> = ClientOptions<Schema>,
in out Schema extends SchemaDef,
// Bounded by `QueryOptions` (not `ClientOptions`): only `omit`/`slicing` shape the schema types.
// The `$zod` accessor passes the client's *projected* (query-relevant) options, so the heavy
// options literal never reaches the (invariant) arg types this factory produces.
in out Options extends QueryOptions<Schema> = ClientOptions<Schema>,
ExtQueryArgs extends ExtQueryArgsBase = {},
> {
private readonly schemaCache = new Map<string, ZodType>();
Expand All @@ -140,7 +143,10 @@ export class ZodSchemaFactory<
private readonly options: Options;
private readonly extraValidationsEnabled = true;

constructor(client: ClientContract<Schema, Options, ExtQueryArgs, any>);
// The client's `Options` type arg is intentionally erased here (`any`) - the factory only needs
// the schema and the runtime options object; its own `Options` type param is supplied (projected)
// by the caller (`$zod`).
constructor(client: ClientContract<Schema, any, ExtQueryArgs, any>);
constructor(schema: Schema, options?: Options);
constructor(clientOrSchema: any, options?: Options) {
if ('$schema' in clientOrSchema) {
Expand All @@ -153,7 +159,9 @@ export class ZodSchemaFactory<
}

private get plugins(): AnyPlugin[] {
return this.options.plugins ?? [];
// `plugins` is a runtime-only field absent from the query-relevant `Options` type;
// read it weakly (the concrete options object always carries it at runtime).
return (this.options as { plugins?: AnyPlugin[] }).plugins ?? [];
}

/**
Expand Down
Loading
Loading