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
49 changes: 25 additions & 24 deletions packages/orm/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1157,24 +1157,22 @@ export type FtsRelevanceOrderBy<Schema extends SchemaDef, Model extends GetModel
};

/**
* The query-time arguments object of a parameterized computed field, derived from the
* generated `computedFields` stub signature `(context, args) => R` — the same source
* `ComputedFieldsOptions` reads, so the implementation signature and the query-time args
* can never drift apart. Resolves to `never` for non-parameterized fields.
* The query-time arguments object of a parameterized computed field, derived from the field's
* `params` metadata in the schema — the same source the runtime forwards to the implementation
* and the zod factory validates against, so typing and validation can never drift apart. Param
* types resolve the way procedure params do: scalars to their TS types, enums to their value
* union, type defs to their object shape. `ComputedFieldsOptions` reads this too, so the
* implementation signature always matches the query input. Resolves to `never` for
* non-parameterized fields.
*/
export type ComputedFieldArgs<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Field extends GetModelFields<Schema, Model>,
> = 'computedFields' extends keyof GetModel<Schema, Model>
? Field extends keyof GetModel<Schema, Model>['computedFields']
? GetModel<Schema, Model>['computedFields'][Field] extends (...args: infer P) => any
? P extends [any, infer Args]
? Args
: never
: never
: never
: never;
> =
GetModelField<Schema, Model, Field> extends { computed: true; params: infer Params }
? MapParamsObject<Schema, Params>
: never;

/**
* Whether `Field` is a parameterized computed field (its args object is not `never`).
Expand Down Expand Up @@ -2897,22 +2895,25 @@ export type GetProcedure<Schema extends SchemaDef, ProcName extends GetProcedure
? Schema['procedures'][ProcName]
: never;

type _OptionalProcedureParamNames<Params> = keyof {
// The `params` metadata record (`{ name, type, array?, optional? }` per key) is shared by
// procedures and parameterized computed fields; these helpers map it to the TS args object.

type _OptionalParamNames<Params> = keyof {
[K in keyof Params as Params[K] extends { optional: true } ? K : never]: K;
};

type _RequiredProcedureParamNames<Params> = keyof {
type _RequiredParamNames<Params> = keyof {
[K in keyof Params as Params[K] extends { optional: true } ? never : K]: K;
};

type _HasRequiredProcedureParams<Params> = _RequiredProcedureParamNames<Params> extends never ? false : true;
type _HasRequiredParams<Params> = _RequiredParamNames<Params> extends never ? false : true;

type MapProcedureArgsObject<Schema extends SchemaDef, Params> = Simplify<
type MapParamsObject<Schema extends SchemaDef, Params> = Simplify<
Optional<
{
[K in keyof Params]: MapProcedureParam<Schema, Params[K]>;
[K in keyof Params]: MapParam<Schema, Params[K]>;
},
_OptionalProcedureParamNames<Params>
_OptionalParamNames<Params>
>
>;

Expand All @@ -2923,11 +2924,11 @@ export type ProcedureEnvelope<
> = keyof Params extends never
? // no params
{ args?: Record<string, never> }
: _HasRequiredProcedureParams<Params> extends true
: _HasRequiredParams<Params> extends true
? // has required params
{ args: MapProcedureArgsObject<Schema, Params> }
{ args: MapParamsObject<Schema, Params> }
: // no required params
{ args?: MapProcedureArgsObject<Schema, Params> };
{ args?: MapParamsObject<Schema, Params> };

type ProcedureHandlerCtx<Schema extends SchemaDef, ProcName extends GetProcedureNames<Schema>> = {
client: ClientContract<Schema>;
Expand All @@ -2937,7 +2938,7 @@ type ProcedureHandlerCtx<Schema extends SchemaDef, ProcName extends GetProcedure
* Shape of a procedure's runtime function.
*/
export type ProcedureFunc<Schema extends SchemaDef, ProcName extends GetProcedureNames<Schema>> = (
...args: _HasRequiredProcedureParams<GetProcedureParams<Schema, ProcName>> extends true
...args: _HasRequiredParams<GetProcedureParams<Schema, ProcName>> extends true
? [input: ProcedureEnvelope<Schema, ProcName>]
: [input?: ProcedureEnvelope<Schema, ProcName>]
) => MaybePromise<MapProcedureReturn<Schema, GetProcedure<Schema, ProcName>>>;
Expand All @@ -2955,7 +2956,7 @@ type MapProcedureReturn<Schema extends SchemaDef, Proc> = Proc extends { returnT
: MapType<Schema, R & string>
: never;

type MapProcedureParam<Schema extends SchemaDef, P> = P extends { type: infer U }
type MapParam<Schema extends SchemaDef, P> = P extends { type: infer U }
? OrUndefinedIf<
P extends { array: true } ? Array<MapType<Schema, U & string>> : MapType<Schema, U & string>,
P extends { optional: true } ? true : false
Expand Down
20 changes: 16 additions & 4 deletions packages/orm/src/client/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { GetModel, GetModelFields, GetModels, ProcedureDef, ScalarFields, S
import type { Dialect, Expression, ExpressionBuilder, KyselyConfig, OperandExpression } from 'kysely';
import type { FilterPropertyToKind } from './constants';
import type { ClientContract, CRUD_EXT } from './contract';
import type { GetProcedureNames, ProcedureHandlerFunc } from './crud-types';
import type { ComputedFieldArgs, FieldHasComputedArgs, GetProcedureNames, ProcedureHandlerFunc } from './crud-types';
import type { BaseCrudDialect } from './crud/dialects/base-dialect';
import type { AllCrudOperations } from './crud/operations/base';
import type { AnyPlugin } from './plugin';
Expand Down Expand Up @@ -304,21 +304,33 @@ export type ComputedFieldsOptions<Schema extends SchemaDef> = {
? Uncapitalize<Model>
: never]: {
[Field in keyof Schema['models'][Model]['computedFields']]: Schema['models'][Model]['computedFields'][Field] extends infer Func
? Func extends (...args: infer Params) => infer R
? Func extends (...args: any[]) => infer R
? (
// inject a first parameter for expression builder
p: ExpressionBuilder<ToKyselySchema<Schema>, Model>,
// runtime-provided context (the generated stub only declares
// `modelAlias`; the runtime passes the full context)
context: ComputedFieldContext<Schema>,
// query-time args of a parameterized field, from the stub
...args: Params extends [any, ...infer Rest] ? Rest : []
// query-time args of a parameterized field, typed from the field's
// `params` metadata — the same source as the query input types
...args: ComputedFieldImplArgs<Schema, Model, Field>
) => OperandExpression<R> // wrap the return type with Kysely `OperandExpression`
: never
: never;
};
};

/**
* The trailing parameter list of a computed field implementation: `[args]` for a parameterized
* field, empty otherwise.
*/
type ComputedFieldImplArgs<Schema extends SchemaDef, Model extends GetModels<Schema>, Field> =
Field extends GetModelFields<Schema, Model>
? FieldHasComputedArgs<Schema, Model, Field> extends true
? [args: ComputedFieldArgs<Schema, Model, Field>]
: []
: [];

export type HasComputedFields<Schema extends SchemaDef> =
string extends GetModels<Schema> ? false : keyof ComputedFieldsOptions<Schema> extends never ? false : true;

Expand Down
72 changes: 50 additions & 22 deletions packages/sdk/src/ts-schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,16 +575,18 @@ export class TsSchemaGenerator {
),
];

// For a parameterized computed field, add `args: { <param>: <type> }`.
// The field's params flow into this stub's signature so that
// `Parameters<typeof stub>` carries the args type for both the
// implementation (ComputedFieldsOptions) and the query input types.
// For a parameterized computed field, add `_args: { <param>: <type> }` so the
// stub documents the query-time args. The authoritative typing is the field's
// `params` metadata (see `createFieldParamsObject`), which the ORM maps to the
// args type for both the implementation (`ComputedFieldsOptions`) and the query
// input types. The underscore prefix keeps `noUnusedParameters` quiet in
// consuming projects.
if (field.params.length > 0) {
params.push(
ts.factory.createParameterDeclaration(
undefined,
undefined,
'args',
'_args',
undefined,
ts.factory.createTypeLiteralNode(
field.params.map((param) =>
Expand All @@ -594,9 +596,7 @@ export class TsSchemaGenerator {
param.optional
? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
: undefined,
ts.factory.createTypeReferenceNode(
this.mapFunctionParamTypeToTSType(param.type),
),
this.createFunctionParamTypeNode(param.type),
),
),
),
Expand Down Expand Up @@ -656,25 +656,53 @@ export class TsSchemaGenerator {
);
}

private mapFunctionParamTypeToTSType(type: FunctionParamType): string {
let result = match(type.type)
.with('String', () => 'string')
.with('Boolean', () => 'boolean')
.with('Int', () => 'number')
.with('Float', () => 'number')
.with('BigInt', () => 'bigint')
.with('Decimal', () => 'number')
.with('DateTime', () => 'Date')
// non-scalar references (enums/type defs/models) aren't in scope in the generated
// schema file, so fall back to `unknown` — same convention as computed-field return
// types (`mapFieldTypeToTSType`). Runtime zod still validates these precisely.
.otherwise(() => 'unknown');
// Builds the TS type node of a param in the computed-field stub signature. Scalars map to
// their TS types; an enum maps to its value union, read off the schema's own `enums` member
// so it can't drift from the emitted enum. Type defs and models have no TS type in scope in
// the generated schema file, so they fall back to `unknown` — same convention as
// computed-field return types (`mapFieldTypeToTSType`). The ORM's `ComputedFieldArgs`
// resolves all of them precisely from the `params` metadata, and runtime zod validates them.
private createFunctionParamTypeNode(type: FunctionParamType): ts.TypeNode {
let result: ts.TypeNode;
if (type.reference?.ref && isEnum(type.reference.ref)) {
result = this.createEnumValuesTypeNode(type.reference.ref.name);
} else {
const tsType = match(type.type)
.with('String', () => 'string')
.with('Boolean', () => 'boolean')
.with('Int', () => 'number')
.with('Float', () => 'number')
.with('BigInt', () => 'bigint')
.with('Decimal', () => 'number')
.with('DateTime', () => 'Date')
.otherwise(() => 'unknown');
result = ts.factory.createTypeReferenceNode(tsType);
}
if (type.array) {
result = `${result}[]`;
result = ts.factory.createArrayTypeNode(result);
}
return result;
}

// `SchemaType["enums"]["<Enum>"]["values"][keyof SchemaType["enums"]["<Enum>"]["values"]]`
private createEnumValuesTypeNode(enumName: string): ts.TypeNode {
const literal = (text: string) => ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(text));
const values = ts.factory.createIndexedAccessTypeNode(
ts.factory.createIndexedAccessTypeNode(
ts.factory.createIndexedAccessTypeNode(
ts.factory.createTypeReferenceNode('SchemaType'),
literal('enums'),
),
literal(enumName),
),
literal('values'),
);
return ts.factory.createIndexedAccessTypeNode(
values,
ts.factory.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, values),
);
}

private createUpdatedAtObject(ignoreArg: AttributeArg) {
return ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(
Expand Down
Loading
Loading