Skip to content

Commit 23adf91

Browse files
authored
fix: make computed fields on base models work (#321)
* fix: make computed fields on base models work fixes #284 * update
1 parent 14eaf36 commit 23adf91

File tree

4 files changed

+73
-54
lines changed

4 files changed

+73
-54
lines changed

packages/runtime/src/client/crud/dialects/base-dialect.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { InternalError, QueryError } from '../../errors';
1919
import type { ClientOptions } from '../../options';
2020
import {
2121
aggregate,
22-
buildFieldRef,
2322
buildJoinPairs,
2423
ensureArray,
2524
flattenCompoundUniqueFilters,
@@ -931,15 +930,13 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
931930
field: string,
932931
): SelectQueryBuilder<any, any, any> {
933932
const fieldDef = requireField(this.schema, model, field);
934-
if (fieldDef.computed) {
935-
// TODO: computed field from delegate base?
936-
return query.select(() => this.fieldRef(model, field, modelAlias).as(field));
937-
} else if (!fieldDef.originModel) {
938-
// regular field
939-
return query.select(this.eb.ref(`${modelAlias}.${field}`).as(field));
940-
} else {
941-
return this.buildSelectField(query, fieldDef.originModel, fieldDef.originModel, field);
942-
}
933+
934+
// if field is defined on a delegate base, the base model is joined with its
935+
// model name from outer query, so we should use it directly as the alias
936+
const fieldModel = fieldDef.originModel ?? model;
937+
const alias = fieldDef.originModel ?? modelAlias;
938+
939+
return query.select(() => this.fieldRef(fieldModel, field, alias).as(field));
943940
}
944941

945942
buildDelegateJoin(
@@ -1071,7 +1068,26 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
10711068
}
10721069

10731070
fieldRef(model: string, field: string, modelAlias?: string, inlineComputedField = true) {
1074-
return buildFieldRef(this.schema, model, field, this.options, this.eb, modelAlias, inlineComputedField);
1071+
const fieldDef = requireField(this.schema, model, field);
1072+
1073+
if (!fieldDef.computed) {
1074+
// regular field
1075+
return this.eb.ref(modelAlias ? `${modelAlias}.${field}` : field);
1076+
} else {
1077+
// computed field
1078+
if (!inlineComputedField) {
1079+
return this.eb.ref(modelAlias ? `${modelAlias}.${field}` : field);
1080+
}
1081+
let computer: Function | undefined;
1082+
if ('computedFields' in this.options) {
1083+
const computedFields = this.options.computedFields as Record<string, any>;
1084+
computer = computedFields?.[fieldDef.originModel ?? model]?.[field];
1085+
}
1086+
if (!computer) {
1087+
throw new QueryError(`Computed field "${field}" implementation not provided for model "${model}"`);
1088+
}
1089+
return computer(this.eb, { modelAlias });
1090+
}
10751091
}
10761092

10771093
protected canJoinWithoutNestedSelect(

packages/runtime/src/client/query-utils.ts

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
TableNode,
77
type Expression,
88
type ExpressionBuilder,
9-
type ExpressionWrapper,
109
type OperationNode,
1110
} from 'kysely';
1211
import { match } from 'ts-pattern';
@@ -15,7 +14,6 @@ import { extractFields } from '../utils/object-utils';
1514
import type { AGGREGATE_OPERATORS } from './constants';
1615
import type { OrderBy } from './crud-types';
1716
import { InternalError, QueryError } from './errors';
18-
import type { ClientOptions } from './options';
1917

2018
export function hasModel(schema: SchemaDef, model: string) {
2119
return Object.keys(schema.models)
@@ -180,34 +178,6 @@ export function getIdValues(schema: SchemaDef, model: string, data: any): Record
180178
return idFields.reduce((acc, field) => ({ ...acc, [field]: data[field] }), {});
181179
}
182180

183-
export function buildFieldRef<Schema extends SchemaDef>(
184-
schema: Schema,
185-
model: string,
186-
field: string,
187-
options: ClientOptions<Schema>,
188-
eb: ExpressionBuilder<any, any>,
189-
modelAlias?: string,
190-
inlineComputedField = true,
191-
): ExpressionWrapper<any, any, unknown> {
192-
const fieldDef = requireField(schema, model, field);
193-
if (!fieldDef.computed) {
194-
return eb.ref(modelAlias ? `${modelAlias}.${field}` : field);
195-
} else {
196-
if (!inlineComputedField) {
197-
return eb.ref(modelAlias ? `${modelAlias}.${field}` : field);
198-
}
199-
let computer: Function | undefined;
200-
if ('computedFields' in options) {
201-
const computedFields = options.computedFields as Record<string, any>;
202-
computer = computedFields?.[model]?.[field];
203-
}
204-
if (!computer) {
205-
throw new QueryError(`Computed field "${field}" implementation not provided for model "${model}"`);
206-
}
207-
return computer(eb, { modelAlias });
208-
}
209-
}
210-
211181
export function fieldHasDefaultValue(fieldDef: FieldDef) {
212182
return fieldDef.default !== undefined || fieldDef.updatedAt;
213183
}

samples/blog/zenstack/schema.zmodel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ enum Role {
1010
}
1111

1212
plugin policy {
13-
// due to pnpm layout we can't directly use package name here
13+
// due to pnpm layout we can't directly use package name here,
14+
// don't do this in your code and use "@zenstackhq/plugin-policy" instead
1415
provider = '../node_modules/@zenstackhq/plugin-policy/dist/index.js'
1516
}
1617

tests/e2e/orm/client-api/computed-fields.test.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
import { createTestClient } from '@zenstackhq/testtools';
2-
import { afterEach, describe, expect, it } from 'vitest';
2+
import { describe, expect, it } from 'vitest';
33

44
describe('Computed fields tests', () => {
5-
let db: any;
6-
7-
afterEach(async () => {
8-
await db?.$disconnect();
9-
});
10-
115
it('works with non-optional fields', async () => {
12-
db = await createTestClient(
6+
const db = await createTestClient(
137
`
148
model User {
159
id Int @id @default(autoincrement())
@@ -97,7 +91,7 @@ model User {
9791
});
9892

9993
it('is typed correctly for non-optional fields', async () => {
100-
db = await createTestClient(
94+
await createTestClient(
10195
`
10296
model User {
10397
id Int @id @default(autoincrement())
@@ -137,7 +131,7 @@ main();
137131
});
138132

139133
it('works with optional fields', async () => {
140-
db = await createTestClient(
134+
const db = await createTestClient(
141135
`
142136
model User {
143137
id Int @id @default(autoincrement())
@@ -164,7 +158,7 @@ model User {
164158
});
165159

166160
it('is typed correctly for optional fields', async () => {
167-
db = await createTestClient(
161+
await createTestClient(
168162
`
169163
model User {
170164
id Int @id @default(autoincrement())
@@ -203,7 +197,7 @@ main();
203197
});
204198

205199
it('works with read from a relation', async () => {
206-
db = await createTestClient(
200+
const db = await createTestClient(
207201
`
208202
model User {
209203
id Int @id @default(autoincrement())
@@ -240,4 +234,42 @@ model Post {
240234
author: expect.objectContaining({ postCount: 1 }),
241235
});
242236
});
237+
238+
it('allows sub models to use computed fields from delegate base', async () => {
239+
const db = await createTestClient(
240+
`
241+
model Content {
242+
id Int @id @default(autoincrement())
243+
title String
244+
isNews Boolean @computed
245+
contentType String
246+
@@delegate(contentType)
247+
}
248+
249+
model Post extends Content {
250+
body String
251+
}
252+
`,
253+
{
254+
computedFields: {
255+
Content: {
256+
isNews: (eb: any) => eb('title', 'like', '%news%'),
257+
},
258+
},
259+
} as any,
260+
);
261+
262+
const posts = await db.post.createManyAndReturn({
263+
data: [
264+
{ id: 1, title: 'latest news', body: 'some news content' },
265+
{ id: 2, title: 'random post', body: 'some other content' },
266+
],
267+
});
268+
expect(posts).toEqual(
269+
expect.arrayContaining([
270+
expect.objectContaining({ id: 1, isNews: true }),
271+
expect.objectContaining({ id: 2, isNews: false }),
272+
]),
273+
);
274+
});
243275
});

0 commit comments

Comments
 (0)