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
8 changes: 6 additions & 2 deletions packages/orm/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2389,11 +2389,15 @@ type NumericFields<Schema extends SchemaDef, Model extends GetModels<Schema>> =
: never]: GetModelField<Schema, Model, Key>;
};

// in orderBy position (ValueType = SortOrder) a parameterized computed field entry also
// carries the sort spec alongside its `args`
type AggComputedFieldSortPart<ValueType> = ValueType extends SortOrder ? { sort: SortOrder; nulls?: NullsOrder } : {};

type SumAvgInput<Schema extends SchemaDef, Model extends GetModels<Schema>, ValueType> = {
// a parameterized computed field is aggregated by supplying its query-time `args`
[Key in NumericFields<Schema, Model>]?: Key extends GetModelFields<Schema, Model>
? FieldHasComputedArgs<Schema, Model, Key> extends true
? { args: ComputedFieldArgs<Schema, Model, Key> }
? { args: ComputedFieldArgs<Schema, Model, Key> } & AggComputedFieldSortPart<ValueType>
: ValueType
: ValueType;
};
Expand All @@ -2404,7 +2408,7 @@ type MinMaxInput<Schema extends SchemaDef, Model extends GetModels<Schema>, Valu
: FieldIsRelation<Schema, Model, Key> extends true
? never
: Key]?: FieldHasComputedArgs<Schema, Model, Key> extends true // a parameterized computed field is aggregated by supplying its query-time `args`
? { args: ComputedFieldArgs<Schema, Model, Key> }
? { args: ComputedFieldArgs<Schema, Model, Key> } & AggComputedFieldSortPart<ValueType>
: ValueType;
};

Expand Down
26 changes: 18 additions & 8 deletions packages/orm/src/client/crud/dialects/base-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1285,16 +1285,26 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
field: AggregateOperators,
value: any,
negated: boolean,
buildFieldRef: (model: string, field: string, modelAlias: string) => Expression<any>,
buildFieldRef: (model: string, field: string, modelAlias: string, computedArgs?: unknown) => Expression<any>,
): SelectQueryBuilder<any, any, any> {
invariant(typeof value === 'object', `invalid orderBy value for field "${field}"`);
if (!isPlainObject(value)) {
throw createInvalidInputError(`invalid orderBy value for field "${field}"`);
}
let result = query;
for (const [k, v] of Object.entries<SortOrder>(value)) {
invariant(v === 'asc' || v === 'desc', `invalid orderBy value for field "${field}"`);
result = result.orderBy(
(eb) => aggregate(eb, buildFieldRef(model, k, modelAlias), field),
this.negateSort(v, negated),
);
for (const [k, v] of Object.entries<any>(value)) {
// entry value is either a plain sort order, or an object carrying `sort`/`nulls` and
// the query-time `args` of a parameterized computed field
const sort = isPlainObject(v) ? v['sort'] : v;
if (sort !== 'asc' && sort !== 'desc') {
throw createInvalidInputError(`invalid orderBy value for field "${field}"`);
}
const computedArgs = isPlainObject(v) && 'args' in v ? v['args'] : undefined;
const aggExpr = aggregate(this.eb, buildFieldRef(model, k, modelAlias, computedArgs), field);
const nulls = isPlainObject(v) ? v['nulls'] : undefined;
result =
nulls === 'first' || nulls === 'last'
? this.buildOrderByField(result, aggExpr, this.negateSort(sort, negated), nulls)
: result.orderBy(aggExpr, this.negateSort(sort, negated));
}
return result;
}
Expand Down
63 changes: 63 additions & 0 deletions tests/e2e/orm/client-api/computed-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,69 @@ model Product {
]);
});

it('works with parameterized computed fields in groupBy orderBy aggregations', async () => {
const db = await createTestClient(
`
model Product {
id Int @id @default(autoincrement())
category String
price Int
discounted(off: Int) Int @computed
}
`,
{
computedFields: {
Product: {
// price minus a query-time discount, as a row-local expression
discounted: (_eb: any, ctx: any, args: any) =>
sql<number>`${sql.ref(`${ctx.modelAlias}.price`)} - ${args.off}`,
},
},
} as any,
);

// A: prices [30, 30]; B: prices [50]
await db.product.createMany({
data: [
{ category: 'A', price: 30 },
{ category: 'A', price: 30 },
{ category: 'B', price: 50 },
],
});

// off=0 ⇒ sum A=60, B=50 ⇒ desc [A, B]
await expect(
db.product.groupBy({
by: ['category'],
orderBy: { _sum: { discounted: { args: { off: 0 }, sort: 'desc' } } },
}),
).resolves.toMatchObject([{ category: 'A' }, { category: 'B' }]);

// off=25 ⇒ sum A=10, B=25 ⇒ desc [B, A] (a different arg produces a different order)
await expect(
db.product.groupBy({
by: ['category'],
orderBy: { _sum: { discounted: { args: { off: 25 }, sort: 'desc' } } },
}),
).resolves.toMatchObject([{ category: 'B' }, { category: 'A' }]);

// _min with asc: min A=30, B=50 ⇒ [A, B]
await expect(
db.product.groupBy({
by: ['category'],
orderBy: { _min: { discounted: { args: { off: 0 }, sort: 'asc' } } },
}),
).resolves.toMatchObject([{ category: 'A' }, { category: 'B' }]);

// omitting `sort` is rejected by input validation
await expect(
db.product.groupBy({
by: ['category'],
orderBy: { _sum: { discounted: { args: { off: 0 } } } } as any,
}),
).toBeRejectedByValidation();
});

it('works with parameterized computed fields in a nested include/select', async () => {
const db = await createTestClient(
`
Expand Down
Loading