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
42 changes: 22 additions & 20 deletions packages/orm/src/client/client-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,26 +185,28 @@ export class ClientImpl {
'computedFields' in options ? (options.computedFields as Record<string, any> | undefined) : undefined;

for (const [modelName, modelDef] of Object.entries(this.$schema.models)) {
if (modelDef.computedFields) {
for (const fieldName of Object.keys(modelDef.computedFields)) {
// check both uncapitalized (current) and original (backward compat) model name
const modelConfig =
computedFieldsConfig?.[lowerCaseFirst(modelName)] ?? computedFieldsConfig?.[modelName];
const fieldConfig = modelConfig?.[fieldName];
// Check if the computed field has a configuration
if (fieldConfig === null || fieldConfig === undefined) {
throw createConfigError(
`Computed field "${fieldName}" in model "${modelName}" does not have a configuration. ` +
`Please provide an implementation in the computedFields option.`,
);
}
// Check that the configuration is a function
if (typeof fieldConfig !== 'function') {
throw createConfigError(
`Computed field "${fieldName}" in model "${modelName}" has an invalid configuration: ` +
`expected a function but received ${typeof fieldConfig}.`,
);
}
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
// a computed field inherited from a delegate base is configured on the base model
if (!fieldDef.computed || fieldDef.originModel) {
continue;
}
// check both uncapitalized (current) and original (backward compat) model name
const modelConfig =
computedFieldsConfig?.[lowerCaseFirst(modelName)] ?? computedFieldsConfig?.[modelName];
const fieldConfig = modelConfig?.[fieldName];
// Check if the computed field has a configuration
if (fieldConfig === null || fieldConfig === undefined) {
throw createConfigError(
`Computed field "${fieldName}" in model "${modelName}" does not have a configuration. ` +
`Please provide an implementation in the computedFields option.`,
);
}
// Check that the configuration is a function
if (typeof fieldConfig !== 'function') {
throw createConfigError(
`Computed field "${fieldName}" in model "${modelName}" has an invalid configuration: ` +
`expected a function but received ${typeof fieldConfig}.`,
);
}
}
}
Expand Down
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
4 changes: 2 additions & 2 deletions packages/orm/src/client/crud/dialects/base-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1700,8 +1700,8 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
modelDef: ModelDef,
payload: boolean | FindArgs<Schema, GetModels<Schema>, any, true>,
) {
if (modelDef.computedFields) {
// computed fields requires explicit select
if (Object.values(modelDef.fields).some((f) => f.computed)) {
// computed fields require explicit select
return false;
}

Expand Down
94 changes: 76 additions & 18 deletions packages/orm/src/client/options.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import type { GetModel, GetModelFields, GetModels, ProcedureDef, ScalarFields, SchemaDef } from '@zenstackhq/schema';
import type {
FieldIsArray,
GetModelField,
GetModelFields,
GetModelFieldType,
GetModels,
ModelFieldIsOptional,
ProcedureDef,
ScalarFields,
SchemaDef,
} from '@zenstackhq/schema';
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';
import type { ToKyselySchema } from './query-builder';
import type { WrapType } from '../utils/type-utils';

export type ZModelFunctionContext<Schema extends SchemaDef> = {
/**
Expand Down Expand Up @@ -299,26 +310,73 @@ export type ComputedFieldContext<Schema extends SchemaDef> = {
client: ClientContract<Schema>;
};

/**
* The computed fields a model declares itself, keyed by name. A computed field inherited from a
* delegate base is excluded: it's configured once, on the base model.
*/
type OwnComputedFields<Schema extends SchemaDef, Model extends GetModels<Schema>> = keyof {
[Field in GetModelFields<Schema, Model> as GetModelField<Schema, Model, Field> extends { computed: true }
? GetModelField<Schema, Model, Field> extends { originModel: string }
? never
: Field
: never]: Field;
};

/**
* Implementations of the schema's computed fields, keyed by (uncapitalized) model name and then
* by field name. Everything is derived from the field definitions: which fields need an
* implementation, the query-time `args` of a parameterized field (from its `params` metadata,
* the same source the query input types use), and the value type the expression must produce.
*/
export type ComputedFieldsOptions<Schema extends SchemaDef> = {
[Model in GetModels<Schema> as 'computedFields' extends keyof GetModel<Schema, Model>
? 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
? (
// 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 : []
) => OperandExpression<R> // wrap the return type with Kysely `OperandExpression`
: never
: never;
[Model in GetModels<Schema> as [OwnComputedFields<Schema, Model>] extends [never] ? never : Uncapitalize<Model>]: {
[Field in OwnComputedFields<Schema, Model>]: (
// inject a first parameter for expression builder
p: ExpressionBuilder<ToKyselySchema<Schema>, Model>,
// runtime-provided context
context: ComputedFieldContext<Schema>,
// query-time args of a parameterized field
...args: ComputedFieldImplArgs<Schema, Model, Field>
) => OperandExpression<ComputedFieldResultType<Schema, Model, Field>>;
};
};

/**
* 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 extends GetModelFields<Schema, Model>,
> = FieldHasComputedArgs<Schema, Model, Field> extends true ? [args: ComputedFieldArgs<Schema, Model, Field>] : [];

/**
* The value type a computed field's expression must produce, from the field's declared type.
* Scalars map to their JS types (`Decimal` is accepted as `number`); `DateTime`, `Json`,
* `Bytes`, enums and type defs are `unknown`, since their database-level representation differs
* from the ORM result type. An optional field also accepts `null`, a list field an array.
*/
type ComputedFieldResultType<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Field extends GetModelFields<Schema, Model>,
> = WrapType<
ComputedFieldBaseType<GetModelFieldType<Schema, Model, Field>>,
ModelFieldIsOptional<Schema, Model, Field>,
FieldIsArray<Schema, Model, Field>
>;

type ComputedFieldBaseType<T> = T extends 'String'
? string
: T extends 'Boolean'
? boolean
: T extends 'Int' | 'Float' | 'Decimal'
? number
: T extends 'BigInt'
? bigint
: unknown;

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

Expand Down
7 changes: 0 additions & 7 deletions packages/orm/test/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,6 @@ export class SchemaType implements SchemaDef {
idFields: ["id"],
uniqueFields: {
id: { type: "String" }
},
computedFields: {
finalPrice(_context: {
modelAlias: string;
}): number {
throw new Error("This is a stub for computed field");
}
}
},
Asset: {
Expand Down
1 change: 0 additions & 1 deletion packages/schema/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export type ModelDef = {
attributes?: readonly AttributeApplication[];
uniqueFields: Record<string, UniqueFieldsInfo>;
idFields: readonly string[];
computedFields?: Record<string, Function>;
isDelegate?: boolean;
subModels?: readonly string[];
isView?: boolean;
Expand Down
Loading
Loading