@@ -10,9 +10,11 @@ import {
1010 Transaction ,
1111 type KyselyProps ,
1212} from 'kysely' ;
13+ import z from 'zod' ;
1314import type { ProcedureDef , SchemaDef } from '../schema' ;
1415import type { AnyKysely } from '../utils/kysely-utils' ;
1516import type { UnwrapTuplePromises } from '../utils/type-utils' ;
17+ import { formatError } from '../utils/zod-utils' ;
1618import type {
1719 AuthType ,
1820 ClientConstructor ,
@@ -31,6 +33,7 @@ import { FindOperationHandler } from './crud/operations/find';
3133import { GroupByOperationHandler } from './crud/operations/group-by' ;
3234import { UpdateOperationHandler } from './crud/operations/update' ;
3335import { InputValidator } from './crud/validator' ;
36+ import type { Diagnostics , QueryInfo } from './diagnostics' ;
3437import { createConfigError , createNotFoundError , createNotSupportedError } from './errors' ;
3538import { ZenStackDriver } from './executor/zenstack-driver' ;
3639import { ZenStackQueryExecutor } from './executor/zenstack-query-executor' ;
@@ -60,6 +63,7 @@ export class ClientImpl {
6063 readonly kyselyProps : KyselyProps ;
6164 private auth : AuthType < SchemaDef > | undefined ;
6265 inputValidator : InputValidator < SchemaDef > ;
66+ readonly slowQueries : QueryInfo [ ] = [ ] ;
6367
6468 constructor (
6569 private readonly schema : SchemaDef ,
@@ -75,10 +79,7 @@ export class ClientImpl {
7579 ...this . $options . functions ,
7680 } ;
7781
78- if ( ! baseClient && ! options . skipValidationForComputedFields ) {
79- // validate computed fields configuration once for the root client
80- this . validateComputedFieldsConfig ( ) ;
81- }
82+ this . validateOptions ( baseClient , options ) ;
8283
8384 // here we use kysely's props constructor so we can pass a custom query executor
8485 if ( baseClient ) {
@@ -96,6 +97,7 @@ export class ClientImpl {
9697 } ;
9798 this . kyselyRaw = baseClient . kyselyRaw ;
9899 this . auth = baseClient . auth ;
100+ this . slowQueries = baseClient . slowQueries ;
99101 } else {
100102 const driver = new ZenStackDriver ( options . dialect . createDriver ( ) , new Log ( this . $options . log ?? [ ] ) ) ;
101103 const compiler = options . dialect . createQueryCompiler ( ) ;
@@ -127,37 +129,30 @@ export class ClientImpl {
127129 return createClientProxy ( this ) ;
128130 }
129131
130- get $qb ( ) {
131- return this . kysely ;
132- }
133-
134- get $qbRaw ( ) {
135- return this . kyselyRaw ;
136- }
137-
138- get $zod ( ) {
139- return this . inputValidator . zodFactory ;
140- }
141-
142- get isTransaction ( ) {
143- return this . kysely . isTransaction ;
144- }
132+ private validateOptions ( baseClient : ClientImpl | undefined , options : ClientOptions < SchemaDef > ) {
133+ if ( ! baseClient && ! options . skipValidationForComputedFields ) {
134+ // validate computed fields configuration once for the root client
135+ this . validateComputedFieldsConfig ( options ) ;
136+ }
145137
146- /**
147- * Create a new client with a new query executor.
148- */
149- withExecutor ( executor : QueryExecutor ) {
150- return new ClientImpl ( this . schema , this . $options , this , executor ) ;
138+ if ( options . diagnostics ) {
139+ const diagnosticsSchema = z . object ( {
140+ slowQueryThresholdMs : z . number ( ) . nonnegative ( ) . optional ( ) ,
141+ slowQueryMaxRecords : z . int ( ) . nonnegative ( ) . or ( z . literal ( Infinity ) ) . optional ( ) ,
142+ } ) ;
143+ const parseResult = diagnosticsSchema . safeParse ( options . diagnostics ) ;
144+ if ( ! parseResult . success ) {
145+ throw createConfigError ( `Invalid diagnostics configuration: ${ formatError ( parseResult . error ) } ` ) ;
146+ }
147+ }
151148 }
152149
153150 /**
154151 * Validates that all computed fields in the schema have corresponding configurations.
155152 */
156- private validateComputedFieldsConfig ( ) {
153+ private validateComputedFieldsConfig ( options : ClientOptions < SchemaDef > ) {
157154 const computedFieldsConfig =
158- 'computedFields' in this . $options
159- ? ( this . $options . computedFields as Record < string , any > | undefined )
160- : undefined ;
155+ 'computedFields' in options ? ( options . computedFields as Record < string , any > | undefined ) : undefined ;
161156
162157 for ( const [ modelName , modelDef ] of Object . entries ( this . $schema . models ) ) {
163158 if ( modelDef . computedFields ) {
@@ -183,6 +178,29 @@ export class ClientImpl {
183178 }
184179 }
185180
181+ get $qb ( ) {
182+ return this . kysely ;
183+ }
184+
185+ get $qbRaw ( ) {
186+ return this . kyselyRaw ;
187+ }
188+
189+ get $zod ( ) {
190+ return this . inputValidator . zodFactory ;
191+ }
192+
193+ get isTransaction ( ) {
194+ return this . kysely . isTransaction ;
195+ }
196+
197+ /**
198+ * Create a new client with a new query executor.
199+ */
200+ withExecutor ( executor : QueryExecutor ) {
201+ return new ClientImpl ( this . schema , this . $options , this , executor ) ;
202+ }
203+
186204 // overload for interactive transaction
187205 $transaction < T > (
188206 callback : ( tx : ClientContract < SchemaDef > ) => Promise < T > ,
@@ -422,6 +440,13 @@ export class ClientImpl {
422440 return this . $setOptions ( newOptions ) ;
423441 }
424442
443+ async $diagnostics ( ) : Promise < Diagnostics > {
444+ return {
445+ zodCache : this . inputValidator . zodFactory . cacheStats ,
446+ slowQueries : this . slowQueries . map ( ( q ) => ( { ...q } ) ) ,
447+ } ;
448+ }
449+
425450 $executeRaw ( query : TemplateStringsArray , ...values : any [ ] ) {
426451 return createZenStackPromise ( async ( ) => {
427452 const result = await sql ( query , ...values ) . execute ( this . kysely ) ;
0 commit comments