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
38 changes: 38 additions & 0 deletions packages/orm/src/client/client-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export class ClientImpl {
...this.$options.functions,
};

if (!baseClient) {
// validate computed fields configuration once for the root client
this.validateComputedFieldsConfig();
}

// here we use kysely's props constructor so we can pass a custom query executor
if (baseClient) {
this.kyselyProps = {
Expand Down Expand Up @@ -139,6 +144,39 @@ export class ClientImpl {
return new ClientImpl(this.schema, this.$options, this, executor);
}

/**
* Validates that all computed fields in the schema have corresponding configurations.
*/
private validateComputedFieldsConfig() {
const computedFieldsConfig =
'computedFields' in this.$options
? (this.$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)) {
const modelConfig = 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}.`,
);
}
}
}
}
}

// overload for interactive transaction
$transaction<T>(
callback: (tx: ClientContract<SchemaDef>) => Promise<T>,
Expand Down
98 changes: 98 additions & 0 deletions tests/e2e/orm/client-api/computed-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,94 @@ import { sql } from 'kysely';
import { describe, expect, it } from 'vitest';

describe('Computed fields tests', () => {
it('throws error when computed field configuration is missing', async () => {
await expect(
createTestClient(
`
model User {
id Int @id @default(autoincrement())
name String
upperName String @computed
}
`,
{
// missing computedFields configuration
} as any,
),
).rejects.toThrow('Computed field "upperName" in model "User" does not have a configuration');
});

it('throws error when computed field is missing from configuration', async () => {
await expect(
createTestClient(
`
model User {
id Int @id @default(autoincrement())
name String
upperName String @computed
lowerName String @computed
}
`,
{
computedFields: {
User: {
// only providing one of two computed fields
upperName: (eb: any) => eb.fn('upper', ['name']),
},
},
} as any,
),
).rejects.toThrow('Computed field "lowerName" in model "User" does not have a configuration');
});

it('throws error when computed field configuration is not a function', async () => {
await expect(
createTestClient(
`
model User {
id Int @id @default(autoincrement())
name String
upperName String @computed
}
`,
{
computedFields: {
User: {
// providing a string instead of a function
upperName: 'not a function' as any,
},
},
} as any,
),
).rejects.toThrow(
'Computed field "upperName" in model "User" has an invalid configuration: expected a function but received string',
);
});

it('throws error when computed field configuration is a non-function object', async () => {
await expect(
createTestClient(
`
model User {
id Int @id @default(autoincrement())
name String
computed1 String @computed
}
`,
{
computedFields: {
User: {
// providing an object instead of a function
computed1: { key: 'value' } as any,
},
},
} as any,
),
).rejects.toThrow(
'Computed field "computed1" in model "User" has an invalid configuration: expected a function but received object',
);
});

it('works with non-optional fields', async () => {
const db = await createTestClient(
`
Expand Down Expand Up @@ -102,6 +190,11 @@ model User {
}
`,
{
computedFields: {
User: {
upperName: (eb: any) => eb.fn('upper', ['name']),
},
},
extraSourceFiles: {
main: `
import { ZenStackClient } from '@zenstackhq/orm';
Expand Down Expand Up @@ -169,6 +262,11 @@ model User {
}
`,
{
computedFields: {
User: {
upperName: (eb: any) => eb.lit(null),
},
},
extraSourceFiles: {
main: `
import { ZenStackClient } from '@zenstackhq/orm';
Expand Down
Loading