@trpc/next 10.26.0 → 10.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,408 @@
1
+ import { render, waitFor } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { initTRPC } from '@trpc/server';
4
+ import React from 'react';
5
+ import superjson from 'superjson';
6
+ import { z } from 'zod';
7
+ import {
8
+ UseTRPCActionResult,
9
+ experimental_createActionHook,
10
+ experimental_serverActionLink,
11
+ } from './create-action-hook';
12
+ import { experimental_createServerActionHandler } from './server';
13
+
14
+ describe('without transformer', () => {
15
+ const instance = initTRPC
16
+ .context<{
17
+ foo: string;
18
+ }>()
19
+ .create({});
20
+ const { procedure } = instance;
21
+
22
+ const createAction = experimental_createServerActionHandler(instance, {
23
+ createContext() {
24
+ return {
25
+ foo: 'bar',
26
+ };
27
+ },
28
+ });
29
+
30
+ const useAction = experimental_createActionHook({
31
+ links: [experimental_serverActionLink()],
32
+ });
33
+
34
+ test('server actions smoke test', async () => {
35
+ const action = createAction(procedure.mutation((opts) => opts.ctx));
36
+ expect(await action()).toMatchInlineSnapshot(`
37
+ Object {
38
+ "result": Object {
39
+ "data": Object {
40
+ "foo": "bar",
41
+ },
42
+ },
43
+ }
44
+ `);
45
+ });
46
+
47
+ test('normalize FormData', async () => {
48
+ const action = createAction(
49
+ procedure
50
+ .input(
51
+ z.object({
52
+ text: z.string(),
53
+ }),
54
+ )
55
+ .mutation((opts) => `hello ${opts.input.text}` as const),
56
+ );
57
+
58
+ expect(
59
+ await action({
60
+ text: 'there',
61
+ }),
62
+ ).toMatchInlineSnapshot(`
63
+ Object {
64
+ "result": Object {
65
+ "data": "hello there",
66
+ },
67
+ }
68
+ `);
69
+
70
+ const formData = new FormData();
71
+ formData.append('text', 'there');
72
+ expect(await action(formData)).toMatchInlineSnapshot(`
73
+ Object {
74
+ "result": Object {
75
+ "data": "hello there",
76
+ },
77
+ }
78
+ `);
79
+ });
80
+
81
+ test('an actual client', async () => {
82
+ const action = createAction(
83
+ procedure
84
+ .input(
85
+ z.object({
86
+ text: z.string(),
87
+ }),
88
+ )
89
+ .mutation((opts) => `hello ${opts.input.text}` as const),
90
+ );
91
+
92
+ const allStates: Omit<
93
+ UseTRPCActionResult<any>,
94
+ 'mutate' | 'mutateAsync'
95
+ >[] = [] as any[];
96
+
97
+ function MyComponent() {
98
+ const mutation = useAction(action);
99
+ const { mutate, mutateAsync, ...other } = mutation;
100
+ allStates.push(other);
101
+
102
+ return (
103
+ <>
104
+ <button
105
+ role="trigger"
106
+ onClick={() => {
107
+ mutation.mutate({
108
+ text: 'world',
109
+ });
110
+ }}
111
+ >
112
+ click me
113
+ </button>
114
+ </>
115
+ );
116
+ }
117
+
118
+ // mount it
119
+ const utils = render(<MyComponent />);
120
+
121
+ // get the contents of pre
122
+ expect(allStates.at(-1)).toMatchInlineSnapshot(`
123
+ Object {
124
+ "status": "idle",
125
+ }
126
+ `);
127
+
128
+ // click the button
129
+ userEvent.click(utils.getByRole('trigger'));
130
+
131
+ // wait to finish
132
+ await waitFor(() => {
133
+ assert(allStates.at(-1)?.status === 'success');
134
+ });
135
+
136
+ expect(allStates).toMatchInlineSnapshot(`
137
+ Array [
138
+ Object {
139
+ "status": "idle",
140
+ },
141
+ Object {
142
+ "status": "loading",
143
+ },
144
+ Object {
145
+ "data": "hello world",
146
+ "status": "success",
147
+ },
148
+ ]
149
+ `);
150
+
151
+ const lastState = allStates.at(-1);
152
+ assert(lastState?.status === 'success');
153
+ expect(lastState.data).toMatchInlineSnapshot(`"hello world"`);
154
+ });
155
+ });
156
+
157
+ describe('with transformer', () => {
158
+ const instance = initTRPC
159
+ .context<{
160
+ foo: string;
161
+ }>()
162
+ .create({
163
+ transformer: superjson,
164
+ });
165
+ const { procedure } = instance;
166
+
167
+ const createAction = experimental_createServerActionHandler(instance, {
168
+ createContext() {
169
+ return {
170
+ foo: 'bar',
171
+ };
172
+ },
173
+ });
174
+
175
+ const useAction = experimental_createActionHook({
176
+ links: [experimental_serverActionLink()],
177
+ transformer: superjson,
178
+ });
179
+
180
+ test('pass a Date', async () => {
181
+ const action = createAction(
182
+ procedure
183
+ .input(
184
+ z.object({
185
+ date: z.date(),
186
+ }),
187
+ )
188
+ .mutation((opts) => opts.input.date),
189
+ );
190
+
191
+ const allStates: Omit<
192
+ UseTRPCActionResult<any>,
193
+ 'mutate' | 'mutateAsync'
194
+ >[] = [] as any[];
195
+
196
+ function MyComponent() {
197
+ const mutation = useAction(action);
198
+ const { mutate, mutateAsync, ...other } = mutation;
199
+ allStates.push(other);
200
+
201
+ return (
202
+ <>
203
+ <button
204
+ role="trigger"
205
+ onClick={() => {
206
+ mutation.mutate({
207
+ date: new Date(0),
208
+ });
209
+ }}
210
+ >
211
+ click me
212
+ </button>
213
+ </>
214
+ );
215
+ }
216
+
217
+ // mount it
218
+ const utils = render(<MyComponent />);
219
+
220
+ // get the contents of pre
221
+ expect(allStates.at(-1)).toMatchInlineSnapshot(`
222
+ Object {
223
+ "status": "idle",
224
+ }
225
+ `);
226
+
227
+ // click the button
228
+ userEvent.click(utils.getByRole('trigger'));
229
+
230
+ // wait to finish
231
+ await waitFor(() => {
232
+ assert(allStates.at(-1)?.status === 'success');
233
+ });
234
+
235
+ expect(allStates).toMatchInlineSnapshot(`
236
+ Array [
237
+ Object {
238
+ "status": "idle",
239
+ },
240
+ Object {
241
+ "status": "loading",
242
+ },
243
+ Object {
244
+ "data": 1970-01-01T00:00:00.000Z,
245
+ "status": "success",
246
+ },
247
+ ]
248
+ `);
249
+
250
+ const lastState = allStates.at(-1);
251
+ assert(lastState?.status === 'success');
252
+ expect(lastState.data).toMatchInlineSnapshot('1970-01-01T00:00:00.000Z');
253
+ expect(lastState.data).toBeInstanceOf(Date);
254
+ });
255
+
256
+ test('FormData', async () => {
257
+ const action = createAction(
258
+ procedure
259
+ .input(
260
+ z.object({
261
+ text: z.string(),
262
+ }),
263
+ )
264
+ .mutation((opts) => opts.input.text),
265
+ );
266
+
267
+ const allStates: Omit<
268
+ UseTRPCActionResult<any>,
269
+ 'mutate' | 'mutateAsync'
270
+ >[] = [] as any[];
271
+
272
+ function MyComponent() {
273
+ const mutation = useAction(action);
274
+ const { mutate, mutateAsync, ...other } = mutation;
275
+ allStates.push(other);
276
+
277
+ return (
278
+ <>
279
+ <form
280
+ onSubmit={(e) => {
281
+ e.preventDefault();
282
+
283
+ const formData = new FormData(e.currentTarget);
284
+ mutation.mutate(formData);
285
+ }}
286
+ >
287
+ <input type="text" name="text" defaultValue="world" />
288
+ <button role="trigger" type="submit">
289
+ click me
290
+ </button>
291
+ </form>
292
+ </>
293
+ );
294
+ }
295
+
296
+ // mount it
297
+ const utils = render(<MyComponent />);
298
+
299
+ // get the contents of pre
300
+ expect(allStates.at(-1)).toMatchInlineSnapshot(`
301
+ Object {
302
+ "status": "idle",
303
+ }
304
+ `);
305
+
306
+ // click the button
307
+ userEvent.click(utils.getByRole('trigger'));
308
+
309
+ // wait to finish
310
+ await waitFor(() => {
311
+ assert(allStates.at(-1)?.status === 'success');
312
+ });
313
+
314
+ expect(allStates).toMatchInlineSnapshot(`
315
+ Array [
316
+ Object {
317
+ "status": "idle",
318
+ },
319
+ Object {
320
+ "status": "loading",
321
+ },
322
+ Object {
323
+ "data": "world",
324
+ "status": "success",
325
+ },
326
+ ]
327
+ `);
328
+
329
+ const lastState = allStates.at(-1);
330
+ assert(lastState?.status === 'success');
331
+ expect(lastState.data).toMatchInlineSnapshot('"world"');
332
+ });
333
+ });
334
+
335
+ describe('type tests', () => {
336
+ const ignoreErrors = async (fn: () => Promise<unknown> | unknown) => {
337
+ try {
338
+ await fn();
339
+ } catch {
340
+ // ignore
341
+ }
342
+ };
343
+
344
+ const instance = initTRPC
345
+ .context<{
346
+ foo: string;
347
+ }>()
348
+ .create({});
349
+ const { procedure } = instance;
350
+
351
+ const createAction = experimental_createServerActionHandler(instance, {
352
+ createContext() {
353
+ return {
354
+ foo: 'bar',
355
+ };
356
+ },
357
+ });
358
+
359
+ const useAction = experimental_createActionHook({
360
+ links: [experimental_serverActionLink()],
361
+ });
362
+
363
+ test('assert input is sent', async () => {
364
+ ignoreErrors(async () => {
365
+ const action = createAction(
366
+ procedure.input(z.string()).mutation((opts) => opts.input),
367
+ );
368
+ const hook = useAction(action);
369
+ // @ts-expect-error this requires an input
370
+ await action();
371
+ // @ts-expect-error this requires an input
372
+ hook.mutate();
373
+
374
+ // @ts-expect-error this requires an input
375
+ await hook.mutateAsync();
376
+ });
377
+ });
378
+
379
+ test('assert types is correct', async () => {
380
+ ignoreErrors(async () => {
381
+ const action = createAction(
382
+ procedure.input(z.date().optional()).mutation((opts) => opts.input),
383
+ );
384
+ const hook = useAction(action);
385
+ // @ts-expect-error wrong type
386
+ await action('bleh');
387
+ // @ts-expect-error wrong type
388
+ hook.mutate('bleh');
389
+
390
+ hook.mutate();
391
+ await action();
392
+ });
393
+ });
394
+
395
+ test('assert no input', async () => {
396
+ ignoreErrors(async () => {
397
+ const action = createAction(procedure.mutation((opts) => opts.input));
398
+ const hook = useAction(action);
399
+ // @ts-expect-error this takes no input
400
+ await action(null);
401
+ // @ts-expect-error this takes no input
402
+ hook.mutate(null);
403
+
404
+ // @ts-expect-error this takes no input
405
+ await hook.mutateAsync(null);
406
+ });
407
+ });
408
+ });
@@ -1,24 +1,31 @@
1
1
  import {
2
+ CreateTRPCProxyClient,
2
3
  clientCallTypeToProcedureType,
3
4
  createTRPCUntypedClient,
4
5
  } from '@trpc/client';
5
6
  import { AnyRouter } from '@trpc/server';
6
- import { createFlatProxy, createRecursiveProxy } from '@trpc/server/shared';
7
- import {
8
- CreateTRPCNextAppRouter,
9
- CreateTRPCNextAppRouterOptions,
10
- UseProcedureRecord,
11
- createUseProxy,
12
- } from './shared';
13
-
14
- function normalizePromiseArray<TValue>(
15
- promise: Promise<TValue> | Promise<TValue>[],
16
- ) {
17
- if (Array.isArray(promise)) {
18
- return Promise.all(promise);
19
- }
20
- return promise;
21
- }
7
+ import { createRecursiveProxy } from '@trpc/server/shared';
8
+ import { CreateTRPCNextAppRouterOptions } from './shared';
9
+
10
+ export {
11
+ // ts-prune-ignore-next
12
+ experimental_createActionHook,
13
+ // ts-prune-ignore-next
14
+ experimental_serverActionLink,
15
+ // ts-prune-ignore-next
16
+ type UseTRPCActionResult,
17
+ // ts-prune-ignore-next
18
+ type inferActionResultProps,
19
+ } from './create-action-hook';
20
+
21
+ // function normalizePromiseArray<TValue>(
22
+ // promise: Promise<TValue> | Promise<TValue>[],
23
+ // ) {
24
+ // if (Array.isArray(promise)) {
25
+ // return Promise.all(promise);
26
+ // }
27
+ // return promise;
28
+ // }
22
29
 
23
30
  type QueryResult = {
24
31
  data?: unknown;
@@ -31,63 +38,64 @@ export function experimental_createTRPCNextAppDirClient<
31
38
  TRouter extends AnyRouter,
32
39
  >(opts: CreateTRPCNextAppRouterOptions<TRouter>) {
33
40
  const client = createTRPCUntypedClient<TRouter>(opts.config());
34
- const useProxy = createUseProxy<TRouter>(client);
41
+ // const useProxy = createUseProxy<TRouter>(client);
35
42
 
36
43
  const cache = new Map<string, QueryResult>();
37
- return createFlatProxy<CreateTRPCNextAppRouter<TRouter>>((key) => {
38
- if (key === 'use') {
39
- return (
40
- cb: (
41
- t: UseProcedureRecord<TRouter>,
42
- ) => Promise<unknown> | Promise<unknown>[],
43
- ) => {
44
- const promise = normalizePromiseArray(cb(useProxy));
45
- throw promise;
46
- // const [data, setData] = useState<unknown | unknown[]>();
47
-
48
- // useEffect(() => {
49
- // const promise = normalizePromiseArray(cb(useProxy));
50
-
51
- // void promise.then(setData).catch((err) => {
52
- // throw err;
53
- // });
54
- // // eslint-disable-next-line react-hooks/exhaustive-deps
55
- // }, []);
56
-
57
- // return data;
58
- };
59
- }
60
-
61
- return createRecursiveProxy(({ path, args }) => {
62
- const pathCopy = [key, ...path];
63
- const procedureType = clientCallTypeToProcedureType(pathCopy.pop()!);
44
+ // return createFlatProxy<CreateTRPCNextAppRouter<TRouter>>((key) => {
45
+ // if (key === 'use') {
46
+ // return (
47
+ // cb: (
48
+ // t: UseProcedureRecord<TRouter>,
49
+ // ) => Promise<unknown> | Promise<unknown>[],
50
+ // ) => {
51
+ // const promise = normalizePromiseArray(cb(useProxy));
52
+ // throw promise;
53
+ // // const [data, setData] = useState<unknown | unknown[]>();
64
54
 
65
- if (procedureType === 'query') {
66
- const queryCacheKey = JSON.stringify([path, args[0]]);
67
- const cached = cache.get(queryCacheKey);
55
+ // // useEffect(() => {
56
+ // // const promise = normalizePromiseArray(cb(useProxy));
68
57
 
69
- if (cached?.promise) {
70
- return cached.promise;
71
- }
72
- }
58
+ // // void promise.then(setData).catch((err) => {
59
+ // // throw err;
60
+ // // });
61
+ // // // eslint-disable-next-line react-hooks/exhaustive-deps
62
+ // // }, []);
73
63
 
74
- const fullPath = pathCopy.join('.');
64
+ // // return data;
65
+ // };
66
+ // }
75
67
 
76
- const promise: Promise<unknown> = (client as any)[procedureType](
77
- fullPath,
78
- ...args,
79
- );
80
- if (procedureType !== 'query') {
81
- return promise;
82
- }
68
+ return createRecursiveProxy(({ path, args }) => {
69
+ // const pathCopy = [key, ...path];
70
+ const pathCopy = [...path];
71
+ const procedureType = clientCallTypeToProcedureType(pathCopy.pop()!);
83
72
 
73
+ if (procedureType === 'query') {
84
74
  const queryCacheKey = JSON.stringify([path, args[0]]);
75
+ const cached = cache.get(queryCacheKey);
85
76
 
86
- cache.set(queryCacheKey, {
87
- promise,
88
- });
77
+ if (cached?.promise) {
78
+ return cached.promise;
79
+ }
80
+ }
89
81
 
82
+ const fullPath = pathCopy.join('.');
83
+
84
+ const promise: Promise<unknown> = (client as any)[procedureType](
85
+ fullPath,
86
+ ...args,
87
+ );
88
+ if (procedureType !== 'query') {
90
89
  return promise;
90
+ }
91
+
92
+ const queryCacheKey = JSON.stringify([path, args[0]]);
93
+
94
+ cache.set(queryCacheKey, {
95
+ promise,
91
96
  });
92
- });
97
+
98
+ return promise;
99
+ }) as CreateTRPCProxyClient<TRouter>;
100
+ // });
93
101
  }