Skip to content

Commit e29af89

Browse files
committed
feat(trpc): support client helpers for Nuxt
1 parent b4418ac commit e29af89

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+12414
-2458
lines changed

packages/plugins/trpc/res/client/v10/next.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { AnyRouter } from '@trpc/server';
44
import type { NextPageContext } from 'next';
55
import { type CreateTRPCNext, createTRPCNext as _createTRPCNext } from '@trpc/next';
66
import type { DeepOverrideAtPath } from './utils';
7-
import type { ClientType } from '../routers';
87

98
export function createTRPCNext<
109
TRouter extends AnyRouter,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/* eslint-disable */
2+
3+
import type { AnyRouter } from '@trpc/server';
4+
import { createTRPCNuxtClient as _createTRPCNuxtClient } from 'trpc-nuxt/client';
5+
import type { DeepOverrideAtPath } from './utils';
6+
7+
export function createTRPCNuxtClient<TRouter extends AnyRouter, TPath extends string | undefined = undefined>(
8+
opts: Parameters<typeof _createTRPCNuxtClient<TRouter>>[0]
9+
) {
10+
const r = _createTRPCNuxtClient<TRouter>(opts);
11+
return r as DeepOverrideAtPath<typeof r, ClientType<TRouter>, TPath>;
12+
}

packages/plugins/trpc/res/client/v10/react.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { AnyRouter } from '@trpc/server';
44
import type { CreateTRPCReactOptions } from '@trpc/react-query/shared';
55
import { type CreateTRPCReact, createTRPCReact as _createTRPCReact } from '@trpc/react-query';
66
import type { DeepOverrideAtPath } from './utils';
7-
import type { ClientType } from '../routers';
87

98
export function createTRPCReact<
109
TRouter extends AnyRouter,

packages/plugins/trpc/res/client/v10/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,17 @@ export type DeepOverrideAtPath<T, R, Path extends string | undefined = undefined
3030
: Path extends keyof T
3131
? Omit<T, Path> & Record<Path, DeepOverride<T[Path], R>>
3232
: never;
33+
34+
// Utility type from 'trpc-nuxt'
35+
export type KeysOf<T> = Array<T extends T ? (keyof T extends string ? keyof T : never) : never>;
36+
37+
// Utility type from 'trpc-nuxt'
38+
export type PickFrom<T, K extends Array<string>> = T extends Array<any>
39+
? T
40+
: T extends Record<string, any>
41+
? keyof T extends K[number]
42+
? T
43+
: K[number] extends never
44+
? T
45+
: Pick<T, K[number]>
46+
: T;

packages/plugins/trpc/res/client/v11/next.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { AnyTRPCRouter as AnyRouter } from '@trpc/server';
44
import type { NextPageContext } from 'next';
55
import { type CreateTRPCNext, createTRPCNext as _createTRPCNext } from '@trpc/next';
66
import type { DeepOverrideAtPath } from './utils';
7-
import type { ClientType } from '../routers';
87

98
export function createTRPCNext<
109
TRouter extends AnyRouter,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/* eslint-disable */
2+
3+
import type { AnyTRPCRouter as AnyRouter } from '@trpc/server';
4+
import { createTRPCNuxtClient as _createTRPCNuxtClient } from 'trpc-nuxt/client';
5+
import type { DeepOverrideAtPath } from './utils';
6+
7+
export function createTRPCNuxtClient<TRouter extends AnyRouter, TPath extends string | undefined = undefined>(
8+
opts: Parameters<typeof _createTRPCNuxtClient<TRouter>>[0]
9+
) {
10+
const r = _createTRPCNuxtClient<TRouter>(opts);
11+
return r as DeepOverrideAtPath<typeof r, ClientType<TRouter>, TPath>;
12+
}

packages/plugins/trpc/res/client/v11/react.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { AnyTRPCRouter as AnyRouter } from '@trpc/server';
44
import type { CreateTRPCReactOptions } from '@trpc/react-query/shared';
55
import { type CreateTRPCReact, createTRPCReact as _createTRPCReact } from '@trpc/react-query';
66
import type { DeepOverrideAtPath } from './utils';
7-
import type { ClientType } from '../routers';
87

98
export function createTRPCReact<
109
TRouter extends AnyRouter,

packages/plugins/trpc/res/client/v11/utils.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../v10/utils.ts
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import { PluginError, type PluginOptions } from '@zenstackhq/sdk';
2+
import { getPrismaClientImportSpec } from '@zenstackhq/sdk/prisma';
3+
import fs from 'fs';
4+
import { lowerCaseFirst } from 'lower-case-first';
5+
import path from 'path';
6+
import {
7+
InterfaceDeclarationStructure,
8+
Project,
9+
PropertySignatureStructure,
10+
SourceFile,
11+
StructureKind,
12+
} from 'ts-morph';
13+
import { upperCaseFirst } from 'upper-case-first';
14+
import { name } from '..';
15+
import { SupportedClientHelpers } from '../utils';
16+
import * as NextHelpers from './next';
17+
import * as NuxtHelpers from './nuxt';
18+
import * as ReactHelpers from './react';
19+
20+
const helpers = {
21+
react: ReactHelpers,
22+
next: NextHelpers,
23+
nuxt: NuxtHelpers,
24+
};
25+
26+
export function generateClientTypingForModel(
27+
project: Project,
28+
generateClientHelpers: SupportedClientHelpers[],
29+
model: string,
30+
options: PluginOptions,
31+
generateOperations: Array<{ name: string; baseType: string }>,
32+
version: string,
33+
outDir: string
34+
) {
35+
for (const clientType of generateClientHelpers) {
36+
const sf = project.createSourceFile(
37+
path.resolve(outDir, `client/${upperCaseFirst(model)}.${clientType}.type.ts`),
38+
undefined,
39+
{
40+
overwrite: true,
41+
}
42+
);
43+
44+
sf.addStatements([`/* eslint-disable */`]);
45+
46+
generateImports(clientType, sf, options, version);
47+
48+
// generate a `ClientType` interface that contains typing for query/mutation operations
49+
const routerTypingStructure: InterfaceDeclarationStructure = {
50+
kind: StructureKind.Interface,
51+
name: 'ClientType',
52+
isExported: true,
53+
typeParameters: ['AppRouter extends AnyRouter', `Context = AppRouter['_def']['_config']['$types']['ctx']`],
54+
properties: [] as PropertySignatureStructure[],
55+
};
56+
57+
for (const { name: generateOpName, baseType: baseOpType } of generateOperations) {
58+
routerTypingStructure.properties?.push({
59+
kind: StructureKind.PropertySignature,
60+
name: generateOpName,
61+
type: (writer) => {
62+
helpers[clientType].generateProcedureTyping(writer, generateOpName, model, baseOpType, version);
63+
},
64+
});
65+
}
66+
67+
sf.addInterface(routerTypingStructure);
68+
}
69+
}
70+
71+
function generateImports(
72+
clientHelperType: SupportedClientHelpers,
73+
sourceFile: SourceFile,
74+
options: PluginOptions,
75+
version: string
76+
) {
77+
const importingDir = sourceFile.getDirectoryPath();
78+
const prismaImport = getPrismaClientImportSpec(importingDir, options);
79+
sourceFile.addStatements([
80+
`import type { Prisma } from '${prismaImport}';`,
81+
`import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client';`,
82+
]);
83+
84+
// generate framework-specific imports
85+
helpers[clientHelperType].generateRouterTypingImports(sourceFile, version);
86+
}
87+
88+
export function createClientHelperEntries(
89+
project: Project,
90+
outputDir: string,
91+
generateClientHelpers: SupportedClientHelpers[],
92+
models: string[],
93+
version: string
94+
) {
95+
// generate utils
96+
const content = fs.readFileSync(path.join(__dirname, `../res/client/${version}/utils.ts`), 'utf-8');
97+
project.createSourceFile(path.resolve(outputDir, 'client', `utils.ts`), content, {
98+
overwrite: true,
99+
});
100+
101+
for (const client of generateClientHelpers) {
102+
createClientHelperEntryForType(project, client, models, version, outputDir);
103+
}
104+
}
105+
106+
function createClientHelperEntryForType(
107+
project: Project,
108+
clientHelperType: SupportedClientHelpers,
109+
models: string[],
110+
version: string,
111+
outputDir: string
112+
) {
113+
const content = fs.readFileSync(path.join(__dirname, `../res/client/${version}/${clientHelperType}.ts`), 'utf-8');
114+
const sf = project.createSourceFile(path.resolve(outputDir, 'client', `${clientHelperType}.ts`), content, {
115+
overwrite: true,
116+
});
117+
118+
sf.addInterface({
119+
name: 'ClientType',
120+
typeParameters: ['AppRouter extends AnyRouter'],
121+
isExported: true,
122+
properties: models.map((model) => {
123+
sf.addImportDeclaration({
124+
namedImports: [{ name: 'ClientType', alias: `${upperCaseFirst(model)}ClientType` }],
125+
moduleSpecifier: `./${upperCaseFirst(model)}.${clientHelperType}.type`,
126+
});
127+
return {
128+
name: lowerCaseFirst(model),
129+
type: `${upperCaseFirst(model)}ClientType<AppRouter>`,
130+
} as PropertySignatureStructure;
131+
}),
132+
});
133+
}
134+
135+
/**
136+
* Given a model and Prisma operation, returns related TS types.
137+
*/
138+
export function getPrismaOperationTypes(model: string, operation: string) {
139+
// TODO: find a way to derive from Prisma Client API's generic types
140+
// instead of duplicating them
141+
142+
const capModel = upperCaseFirst(model);
143+
const capOperation = upperCaseFirst(operation);
144+
145+
let genericBase = `Prisma.${capModel}${capOperation}Args`;
146+
const getPayload = `Prisma.${capModel}GetPayload<T>`;
147+
const selectSubset = `Prisma.SelectSubset<T, ${genericBase}>`;
148+
149+
let argsType: string;
150+
let resultType: string;
151+
const argsOptional = ['findMany', 'findFirst', 'findFirstOrThrow', 'createMany', 'deleteMany', 'count'].includes(
152+
operation
153+
);
154+
155+
switch (operation) {
156+
case 'findUnique':
157+
case 'findUniqueOrThrow':
158+
case 'findFirst':
159+
case 'findFirstOrThrow':
160+
argsType = selectSubset;
161+
resultType = getPayload;
162+
break;
163+
164+
case 'findMany':
165+
argsType = selectSubset;
166+
resultType = `Array<${getPayload}>`;
167+
break;
168+
169+
case 'create':
170+
argsType = selectSubset;
171+
resultType = getPayload;
172+
break;
173+
174+
case 'createMany':
175+
argsType = selectSubset;
176+
resultType = `Prisma.BatchPayload`;
177+
break;
178+
179+
case 'update':
180+
argsType = selectSubset;
181+
resultType = getPayload;
182+
break;
183+
184+
case 'updateMany':
185+
argsType = selectSubset;
186+
resultType = `Prisma.BatchPayload`;
187+
break;
188+
189+
case 'upsert':
190+
argsType = selectSubset;
191+
resultType = getPayload;
192+
break;
193+
194+
case 'delete':
195+
argsType = selectSubset;
196+
resultType = getPayload;
197+
break;
198+
199+
case 'deleteMany':
200+
argsType = selectSubset;
201+
resultType = `Prisma.BatchPayload`;
202+
break;
203+
204+
case 'count':
205+
argsType = `Prisma.Subset<T, ${genericBase}>`;
206+
resultType = `'select' extends keyof T
207+
? T['select'] extends true
208+
? number
209+
: Prisma.GetScalarType<T['select'], Prisma.${capModel}CountAggregateOutputType>
210+
: number`;
211+
break;
212+
213+
case 'aggregate':
214+
argsType = `Prisma.Subset<T, ${genericBase}>`;
215+
resultType = `Prisma.Get${capModel}AggregateType<T>`;
216+
break;
217+
218+
case 'groupBy':
219+
genericBase = `Prisma.${capModel}GroupByArgs,
220+
HasSelectOrTake extends Prisma.Or<
221+
Prisma.Extends<'skip', Prisma.Keys<T>>,
222+
Prisma.Extends<'take', Prisma.Keys<T>>
223+
>,
224+
OrderByArg extends Prisma.True extends HasSelectOrTake
225+
? { orderBy: Prisma.${capModel}GroupByArgs['orderBy'] }
226+
: { orderBy?: Prisma.${capModel}GroupByArgs['orderBy'] },
227+
OrderFields extends Prisma.ExcludeUnderscoreKeys<Prisma.Keys<Prisma.MaybeTupleToUnion<T['orderBy']>>>,
228+
ByFields extends Prisma.MaybeTupleToUnion<T['by']>,
229+
ByValid extends Prisma.Has<ByFields, OrderFields>,
230+
HavingFields extends Prisma.GetHavingFields<T['having']>,
231+
HavingValid extends Prisma.Has<ByFields, HavingFields>,
232+
ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False,
233+
InputErrors extends ByEmpty extends Prisma.True
234+
? \`Error: "by" must not be empty.\`
235+
: HavingValid extends Prisma.False
236+
? {
237+
[P in HavingFields]: P extends ByFields
238+
? never
239+
: P extends string
240+
? \`Error: Field "\${P}" used in "having" needs to be provided in "by".\`
241+
: [
242+
Error,
243+
'Field ',
244+
P,
245+
\` in "having" needs to be provided in "by"\`,
246+
]
247+
}[HavingFields]
248+
: 'take' extends Prisma.Keys<T>
249+
? 'orderBy' extends Prisma.Keys<T>
250+
? ByValid extends Prisma.True
251+
? {}
252+
: {
253+
[P in OrderFields]: P extends ByFields
254+
? never
255+
: \`Error: Field "\${P}" in "orderBy" needs to be provided in "by"\`
256+
}[OrderFields]
257+
: 'Error: If you provide "take", you also need to provide "orderBy"'
258+
: 'skip' extends Prisma.Keys<T>
259+
? 'orderBy' extends Prisma.Keys<T>
260+
? ByValid extends Prisma.True
261+
? {}
262+
: {
263+
[P in OrderFields]: P extends ByFields
264+
? never
265+
: \`Error: Field "\${P}" in "orderBy" needs to be provided in "by"\`
266+
}[OrderFields]
267+
: 'Error: If you provide "skip", you also need to provide "orderBy"'
268+
: ByValid extends Prisma.True
269+
? {}
270+
: {
271+
[P in OrderFields]: P extends ByFields
272+
? never
273+
: \`Error: Field "\${P}" in "orderBy" needs to be provided in "by"\`
274+
}[OrderFields]
275+
`;
276+
argsType = `Prisma.SubsetIntersection<T, Prisma.${capModel}GroupByArgs, OrderByArg> & InputErrors`;
277+
resultType = `{} extends InputErrors ? Prisma.Get${capModel}GroupByPayload<T> : InputErrors`;
278+
break;
279+
280+
default:
281+
throw new PluginError(name, `Unsupported operation: "${operation}"`);
282+
}
283+
284+
return { genericBase, argsType, resultType, argsOptional };
285+
}

0 commit comments

Comments
 (0)