Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.

Commit 206a296

Browse files
committed
feat: custom procs
1 parent 65388a5 commit 206a296

37 files changed

Lines changed: 2567 additions & 62 deletions

packages/cli/test/ts-schema-gen.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,30 @@ model Post {
184184
});
185185
});
186186

187+
it('generates correct procedures with array params and returns', async () => {
188+
const { schema } = await generateTsSchema(`
189+
model User {
190+
id Int @id
191+
}
192+
193+
procedure findByIds(ids: Int[]): User[]
194+
procedure getIds(): Int[]
195+
`);
196+
197+
expect(schema.procedures).toMatchObject({
198+
findByIds: {
199+
params: [{ name: 'ids', type: 'Int', array: true }],
200+
returnType: 'User',
201+
returnArray: true,
202+
},
203+
getIds: {
204+
params: [],
205+
returnType: 'Int',
206+
returnArray: true,
207+
},
208+
});
209+
});
210+
187211
it('merges fields and attributes from mixins', async () => {
188212
const { schema } = await generateTsSchema(`
189213
type Timestamped {

packages/clients/tanstack-query/src/react.ts

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import type {
3838
FindUniqueArgs,
3939
GroupByArgs,
4040
GroupByResult,
41+
ProcedureFunc,
4142
QueryOptions,
4243
SelectSubset,
4344
SimplifiedPlainResult,
@@ -61,6 +62,25 @@ import type {
6162
} from './common/types';
6263
export type { FetchFn } from '@zenstackhq/client-helpers/fetch';
6364

65+
type ExtractProcedures<Schema extends SchemaDef> = Schema extends { procedures: Record<string, any> }
66+
? NonNullable<Schema['procedures']>
67+
: never;
68+
69+
type ProcedureArgsTuple<Schema extends SchemaDef, Name extends keyof ExtractProcedures<Schema>> = Parameters<
70+
ProcedureFunc<Schema, ExtractProcedures<Schema>[Name]>
71+
>;
72+
73+
type ProcedureReturn<Schema extends SchemaDef, Name extends keyof ExtractProcedures<Schema>> = Awaited<ReturnType<
74+
ProcedureFunc<Schema, ExtractProcedures<Schema>[Name]>
75+
>>;
76+
77+
type ProcedurePayload<Schema extends SchemaDef, Name extends keyof ExtractProcedures<Schema>> =
78+
ProcedureArgsTuple<Schema, Name> extends []
79+
? undefined
80+
: ProcedureArgsTuple<Schema, Name> extends [infer A]
81+
? A
82+
: ProcedureArgsTuple<Schema, Name>;
83+
6484
/**
6585
* React context for query settings.
6686
*/
@@ -133,8 +153,56 @@ export type ModelMutationModelResult<
133153

134154
export type ClientHooks<Schema extends SchemaDef, Options extends QueryOptions<Schema> = QueryOptions<Schema>> = {
135155
[Model in GetModels<Schema> as `${Uncapitalize<Model>}`]: ModelQueryHooks<Schema, Model, Options>;
156+
} & ProcedureHooks<Schema>;
157+
158+
type ProcedureHookGroup<Schema extends SchemaDef> = {
159+
[Name in keyof ExtractProcedures<Schema>]: ExtractProcedures<Schema>[Name] extends { mutation: true }
160+
? {
161+
useMutation(
162+
options?: Omit<
163+
UseMutationOptions<ProcedureReturn<Schema, Name>, DefaultError, ProcedurePayload<Schema, Name>>,
164+
'mutationFn'
165+
> &
166+
QueryContext,
167+
): UseMutationResult<ProcedureReturn<Schema, Name>, DefaultError, ProcedurePayload<Schema, Name>>;
168+
}
169+
: {
170+
useQuery(
171+
args?: ProcedurePayload<Schema, Name>,
172+
options?: ModelQueryOptions<ProcedureReturn<Schema, Name>>,
173+
): ModelQueryResult<ProcedureReturn<Schema, Name>>;
174+
175+
useSuspenseQuery(
176+
args?: ProcedurePayload<Schema, Name>,
177+
options?: ModelSuspenseQueryOptions<ProcedureReturn<Schema, Name>>,
178+
): ModelSuspenseQueryResult<ProcedureReturn<Schema, Name>>;
179+
180+
useInfiniteQuery(
181+
args?: ProcedurePayload<Schema, Name>,
182+
options?: ModelInfiniteQueryOptions<ProcedureReturn<Schema, Name>>,
183+
): ModelInfiniteQueryResult<InfiniteData<ProcedureReturn<Schema, Name>>>;
184+
185+
useSuspenseInfiniteQuery(
186+
args?: ProcedurePayload<Schema, Name>,
187+
options?: ModelSuspenseInfiniteQueryOptions<ProcedureReturn<Schema, Name>>,
188+
): ModelSuspenseInfiniteQueryResult<InfiniteData<ProcedureReturn<Schema, Name>>>;
189+
};
136190
};
137191

192+
export type ProcedureHooks<Schema extends SchemaDef> = Schema extends { procedures: Record<string, any> }
193+
? {
194+
/**
195+
* Preferred procedures API.
196+
*/
197+
$procs: ProcedureHookGroup<Schema>;
198+
199+
/**
200+
* Backward-compatible procedures API.
201+
*/
202+
$procedures: ProcedureHookGroup<Schema>;
203+
}
204+
: {};
205+
138206
// Note that we can potentially use TypeScript's mapped type to directly map from ORM contract, but that seems
139207
// to significantly slow down tsc performance ...
140208
export type ModelQueryHooks<
@@ -263,7 +331,7 @@ export function useClientQueries<Schema extends SchemaDef, Options extends Query
263331
schema: Schema,
264332
options?: QueryContext,
265333
): ClientHooks<Schema, Options> {
266-
return Object.keys(schema.models).reduce(
334+
const result = Object.keys(schema.models).reduce(
267335
(acc, model) => {
268336
(acc as any)[lowerCaseFirst(model)] = useModelQueries<Schema, GetModels<Schema>, Options>(
269337
schema,
@@ -274,6 +342,137 @@ export function useClientQueries<Schema extends SchemaDef, Options extends Query
274342
},
275343
{} as ClientHooks<Schema, Options>,
276344
);
345+
346+
const procedures = (schema as any).procedures as Record<string, { mutation?: boolean }> | undefined;
347+
if (procedures) {
348+
const buildProcedureHooks = (endpointModel: '$procs' | '$procedures') => {
349+
return Object.keys(procedures).reduce((acc, name) => {
350+
const procDef = procedures[name];
351+
if (procDef?.mutation) {
352+
acc[name] = {
353+
useMutation: (hookOptions?: any) =>
354+
useInternalProcedureMutation(schema, endpointModel, name, { ...options, ...hookOptions }),
355+
};
356+
} else {
357+
acc[name] = {
358+
useQuery: (args?: any, hookOptions?: any) =>
359+
useInternalProcedureQuery(schema, endpointModel, name, args, { ...options, ...hookOptions }),
360+
useSuspenseQuery: (args?: any, hookOptions?: any) =>
361+
useInternalProcedureSuspenseQuery(schema, endpointModel, name, args, {
362+
...options,
363+
...hookOptions,
364+
}),
365+
useInfiniteQuery: (args?: any, hookOptions?: any) =>
366+
useInternalProcedureInfiniteQuery(schema, endpointModel, name, args, {
367+
...options,
368+
...hookOptions,
369+
}),
370+
useSuspenseInfiniteQuery: (args?: any, hookOptions?: any) =>
371+
useInternalProcedureSuspenseInfiniteQuery(schema, endpointModel, name, args, {
372+
...options,
373+
...hookOptions,
374+
}),
375+
};
376+
}
377+
return acc;
378+
}, {} as any);
379+
};
380+
381+
(result as any).$procs = buildProcedureHooks('$procs');
382+
(result as any).$procedures = buildProcedureHooks('$procedures');
383+
}
384+
385+
return result;
386+
}
387+
388+
export function useInternalProcedureQuery<TQueryFnData, TData>(
389+
_schema: SchemaDef,
390+
endpointModel: '$procs' | '$procedures',
391+
procedure: string,
392+
args?: unknown,
393+
options?: Omit<UseQueryOptions<TQueryFnData, DefaultError, TData>, 'queryKey'> & ExtraQueryOptions,
394+
) {
395+
const { endpoint, fetch } = useFetchOptions(options);
396+
const reqUrl = makeUrl(endpoint, endpointModel, procedure, args);
397+
const queryKey = getQueryKey(endpointModel, procedure, args, {
398+
infinite: false,
399+
optimisticUpdate: false,
400+
});
401+
return {
402+
queryKey,
403+
...useQuery({
404+
queryKey,
405+
queryFn: ({ signal }) => fetcher<TQueryFnData>(reqUrl, { signal }, fetch),
406+
...options,
407+
}),
408+
};
409+
}
410+
411+
export function useInternalProcedureSuspenseQuery<TQueryFnData, TData>(
412+
_schema: SchemaDef,
413+
endpointModel: '$procs' | '$procedures',
414+
procedure: string,
415+
args?: unknown,
416+
options?: Omit<UseSuspenseQueryOptions<TQueryFnData, DefaultError, TData>, 'queryKey'> & ExtraQueryOptions,
417+
) {
418+
const { endpoint, fetch } = useFetchOptions(options);
419+
const reqUrl = makeUrl(endpoint, endpointModel, procedure, args);
420+
const queryKey = getQueryKey(endpointModel, procedure, args, {
421+
infinite: false,
422+
optimisticUpdate: false,
423+
});
424+
return {
425+
queryKey,
426+
...useSuspenseQuery({
427+
queryKey,
428+
queryFn: ({ signal }) => fetcher<TQueryFnData>(reqUrl, { signal }, fetch),
429+
...options,
430+
}),
431+
};
432+
}
433+
434+
export function useInternalProcedureInfiniteQuery<TQueryFnData, TData>(
435+
schema: SchemaDef,
436+
endpointModel: '$procs' | '$procedures',
437+
procedure: string,
438+
args: unknown,
439+
options:
440+
| (Omit<
441+
UseInfiniteQueryOptions<TQueryFnData, DefaultError, InfiniteData<TData>>,
442+
'queryKey' | 'initialPageParam'
443+
> &
444+
QueryContext)
445+
| undefined,
446+
) {
447+
return useInternalInfiniteQuery(schema, endpointModel, procedure, args, options);
448+
}
449+
450+
export function useInternalProcedureSuspenseInfiniteQuery<TQueryFnData, TData>(
451+
schema: SchemaDef,
452+
endpointModel: '$procs' | '$procedures',
453+
procedure: string,
454+
args: unknown,
455+
options: Omit<
456+
UseSuspenseInfiniteQueryOptions<TQueryFnData, DefaultError, InfiniteData<TData>> & QueryContext,
457+
'queryKey' | 'initialPageParam'
458+
>,
459+
) {
460+
return useInternalSuspenseInfiniteQuery(schema, endpointModel, procedure, args, options);
461+
}
462+
463+
export function useInternalProcedureMutation<TArgs, R = any>(
464+
_schema: SchemaDef,
465+
endpointModel: '$procs' | '$procedures',
466+
procedure: string,
467+
options?: Omit<UseMutationOptions<R, DefaultError, TArgs>, 'mutationFn'> & QueryContext,
468+
) {
469+
const { endpoint, fetch } = useFetchOptions(options);
470+
const mutationFn = (data: any) => {
471+
const reqUrl = makeUrl(endpoint, endpointModel, procedure, data);
472+
return fetcher<R>(reqUrl, { method: 'POST' }, fetch) as Promise<R>;
473+
};
474+
475+
return useMutation({ ...options, mutationFn });
277476
}
278477

279478
/**

0 commit comments

Comments
 (0)