Skip to content

Commit 5d8b09a

Browse files
committed
validate computed field configuration on startup
1 parent e10d1b3 commit 5d8b09a

File tree

2 files changed

+136
-0
lines changed

2 files changed

+136
-0
lines changed

packages/orm/src/client/client-impl.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ export class ClientImpl {
7575
...this.$options.functions,
7676
};
7777

78+
if (!baseClient) {
79+
// validate computed fields configuration once for the root client
80+
this.validateComputedFieldsConfig();
81+
}
82+
7883
// here we use kysely's props constructor so we can pass a custom query executor
7984
if (baseClient) {
8085
this.kyselyProps = {
@@ -139,6 +144,39 @@ export class ClientImpl {
139144
return new ClientImpl(this.schema, this.$options, this, executor);
140145
}
141146

147+
/**
148+
* Validates that all computed fields in the schema have corresponding configurations.
149+
*/
150+
private validateComputedFieldsConfig() {
151+
const computedFieldsConfig =
152+
'computedFields' in this.$options
153+
? (this.$options.computedFields as Record<string, any> | undefined)
154+
: undefined;
155+
156+
for (const [modelName, modelDef] of Object.entries(this.$schema.models)) {
157+
if (modelDef.computedFields) {
158+
for (const fieldName of Object.keys(modelDef.computedFields)) {
159+
const modelConfig = computedFieldsConfig?.[modelName];
160+
const fieldConfig = modelConfig?.[fieldName];
161+
// Check if the computed field has a configuration
162+
if (fieldConfig === null) {
163+
throw createConfigError(
164+
`Computed field "${fieldName}" in model "${modelName}" does not have a configuration. ` +
165+
`Please provide an implementation in the computedFields option.`,
166+
);
167+
}
168+
// Check that the configuration is a function
169+
if (typeof fieldConfig !== 'function') {
170+
throw createConfigError(
171+
`Computed field "${fieldName}" in model "${modelName}" has an invalid configuration: ` +
172+
`expected a function but received ${typeof fieldConfig}.`,
173+
);
174+
}
175+
}
176+
}
177+
}
178+
}
179+
142180
// overload for interactive transaction
143181
$transaction<T>(
144182
callback: (tx: ClientContract<SchemaDef>) => Promise<T>,

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

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,94 @@ import { sql } from 'kysely';
33
import { describe, expect, it } from 'vitest';
44

55
describe('Computed fields tests', () => {
6+
it('throws error when computed field configuration is missing', async () => {
7+
await expect(
8+
createTestClient(
9+
`
10+
model User {
11+
id Int @id @default(autoincrement())
12+
name String
13+
upperName String @computed
14+
}
15+
`,
16+
{
17+
// missing computedFields configuration
18+
} as any,
19+
),
20+
).rejects.toThrow('Computed field "upperName" in model "User" does not have a configuration');
21+
});
22+
23+
it('throws error when computed field is missing from configuration', async () => {
24+
await expect(
25+
createTestClient(
26+
`
27+
model User {
28+
id Int @id @default(autoincrement())
29+
name String
30+
upperName String @computed
31+
lowerName String @computed
32+
}
33+
`,
34+
{
35+
computedFields: {
36+
User: {
37+
// only providing one of two computed fields
38+
upperName: (eb: any) => eb.fn('upper', ['name']),
39+
},
40+
},
41+
} as any,
42+
),
43+
).rejects.toThrow('Computed field "lowerName" in model "User" does not have a configuration');
44+
});
45+
46+
it('throws error when computed field configuration is not a function', async () => {
47+
await expect(
48+
createTestClient(
49+
`
50+
model User {
51+
id Int @id @default(autoincrement())
52+
name String
53+
upperName String @computed
54+
}
55+
`,
56+
{
57+
computedFields: {
58+
User: {
59+
// providing a string instead of a function
60+
upperName: 'not a function' as any,
61+
},
62+
},
63+
} as any,
64+
),
65+
).rejects.toThrow(
66+
'Computed field "upperName" in model "User" has an invalid configuration: expected a function but received string',
67+
);
68+
});
69+
70+
it('throws error when computed field configuration is a non-function object', async () => {
71+
await expect(
72+
createTestClient(
73+
`
74+
model User {
75+
id Int @id @default(autoincrement())
76+
name String
77+
computed1 String @computed
78+
}
79+
`,
80+
{
81+
computedFields: {
82+
User: {
83+
// providing an object instead of a function
84+
computed1: { key: 'value' } as any,
85+
},
86+
},
87+
} as any,
88+
),
89+
).rejects.toThrow(
90+
'Computed field "computed1" in model "User" has an invalid configuration: expected a function but received object',
91+
);
92+
});
93+
694
it('works with non-optional fields', async () => {
795
const db = await createTestClient(
896
`
@@ -102,6 +190,11 @@ model User {
102190
}
103191
`,
104192
{
193+
computedFields: {
194+
User: {
195+
upperName: (eb: any) => eb.fn('upper', ['name']),
196+
},
197+
},
105198
extraSourceFiles: {
106199
main: `
107200
import { ZenStackClient } from '@zenstackhq/orm';
@@ -169,6 +262,11 @@ model User {
169262
}
170263
`,
171264
{
265+
computedFields: {
266+
User: {
267+
upperName: (eb: any) => eb.lit(null),
268+
},
269+
},
172270
extraSourceFiles: {
173271
main: `
174272
import { ZenStackClient } from '@zenstackhq/orm';

0 commit comments

Comments
 (0)