@prairielearn/trpc 0.0.0 → 1.0.0

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +683 -0
  3. package/README.md +16 -36
  4. package/dist/client.d.ts +47 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +29 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/client.test.d.ts +2 -0
  9. package/dist/client.test.d.ts.map +1 -0
  10. package/dist/client.test.js +61 -0
  11. package/dist/client.test.js.map +1 -0
  12. package/dist/express.d.ts +42 -0
  13. package/dist/express.d.ts.map +1 -0
  14. package/dist/express.js +67 -0
  15. package/dist/express.js.map +1 -0
  16. package/dist/express.test.d.ts +2 -0
  17. package/dist/express.test.d.ts.map +1 -0
  18. package/dist/express.test.js +51 -0
  19. package/dist/express.test.js.map +1 -0
  20. package/dist/react.d.ts +26 -0
  21. package/dist/react.d.ts.map +1 -0
  22. package/dist/react.js +23 -0
  23. package/dist/react.js.map +1 -0
  24. package/dist/react.test.d.ts +2 -0
  25. package/dist/react.test.d.ts.map +1 -0
  26. package/dist/react.test.js +35 -0
  27. package/dist/react.test.js.map +1 -0
  28. package/dist/server.d.ts +46 -0
  29. package/dist/server.d.ts.map +1 -0
  30. package/dist/server.js +31 -0
  31. package/dist/server.js.map +1 -0
  32. package/dist/server.test.d.ts +2 -0
  33. package/dist/server.test.d.ts.map +1 -0
  34. package/dist/server.test.js +34 -0
  35. package/dist/server.test.js.map +1 -0
  36. package/package.json +43 -8
  37. package/src/client.test.tsx +84 -0
  38. package/src/client.ts +54 -0
  39. package/src/express.test.ts +66 -0
  40. package/src/express.ts +103 -0
  41. package/src/react.test.tsx +55 -0
  42. package/src/react.tsx +53 -0
  43. package/src/server.test.ts +51 -0
  44. package/src/server.ts +77 -0
  45. package/tsconfig.json +6 -0
  46. package/vitest.config.ts +11 -0
@@ -0,0 +1,51 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc';
3
+ import { assert, describe, expectTypeOf, it } from 'vitest';
4
+
5
+ import type { AppError } from './client.js';
6
+ import { appErrorFormatter, throwAppError } from './server.js';
7
+
8
+ interface WholeErrorMap {
9
+ First: { code: 'FIRST_ONLY'; first: string } | { code: 'SHARED'; shared: string };
10
+ Second: { code: 'SECOND_ONLY'; second: string } | { code: 'SHARED'; shared: string };
11
+ }
12
+
13
+ describe('typed application errors', () => {
14
+ it('serializes typed metadata through the error formatter', () => {
15
+ const error = assert.throws(() => {
16
+ throwAppError<WholeErrorMap['First']>(
17
+ {
18
+ code: 'FIRST_ONLY',
19
+ message: 'Expected failure',
20
+ first: 'detail',
21
+ },
22
+ 'CONFLICT',
23
+ );
24
+ });
25
+ assert.instanceOf(error, TRPCError);
26
+
27
+ const result = appErrorFormatter({
28
+ error,
29
+ shape: {
30
+ message: error.message,
31
+ code: TRPC_ERROR_CODES_BY_KEY.CONFLICT,
32
+ data: { code: 'CONFLICT', httpStatus: 409 },
33
+ },
34
+ });
35
+
36
+ assert.deepEqual(result.data.appError, {
37
+ code: 'FIRST_ONLY',
38
+ message: 'Expected failure',
39
+ first: 'detail',
40
+ });
41
+ });
42
+
43
+ it('keeps whole-map server errors narrower than whole-map client errors', () => {
44
+ type WholeMapThrowInput = Parameters<typeof throwAppError<WholeErrorMap>>[0];
45
+
46
+ expectTypeOf<WholeMapThrowInput['code']>().toEqualTypeOf<'SHARED'>();
47
+ expectTypeOf<AppError<WholeErrorMap>['code']>().toEqualTypeOf<
48
+ 'FIRST_ONLY' | 'SECOND_ONLY' | 'SHARED' | 'UNKNOWN'
49
+ >();
50
+ });
51
+ });
package/src/server.ts ADDED
@@ -0,0 +1,77 @@
1
+ import { type TRPCDefaultErrorShape, TRPCError, type TRPC_ERROR_CODE_KEY } from '@trpc/server';
2
+
3
+ /** Metadata serialized for a typed application-level error. */
4
+ export interface AppErrorBase {
5
+ code: string;
6
+ message: string;
7
+ [key: string]: unknown;
8
+ }
9
+
10
+ type Values<T> = T[keyof T];
11
+
12
+ /** Returns `fail` when `E` is not a member of at least one entry in `Map`. */
13
+ type ErrorInvalidForAny<E, Map> = {
14
+ [K in keyof Map]: E extends Map[K] ? never : 'fail';
15
+ }[keyof Map];
16
+
17
+ /** Computes the error variants shared by every procedure in an error map. */
18
+ type SharedProcedureErrors<Map> =
19
+ Values<Map> extends infer E
20
+ ? E extends unknown
21
+ ? ErrorInvalidForAny<E, Map> extends never
22
+ ? E
23
+ : never
24
+ : never
25
+ : never;
26
+
27
+ type ResolveAppErrorForThrow<T> = T extends { code: string } ? T : SharedProcedureErrors<T>;
28
+
29
+ /** The metadata accepted by {@link throwAppError}. */
30
+ export type AppErrorInput<T> = ResolveAppErrorForThrow<T> & { message: string };
31
+
32
+ /** The tRPC error shape produced by {@link appErrorFormatter}. */
33
+ export interface AppErrorShape extends TRPCDefaultErrorShape {
34
+ data: TRPCDefaultErrorShape['data'] & { appError?: AppErrorBase };
35
+ }
36
+
37
+ class AppError extends TRPCError {
38
+ constructor(
39
+ public readonly meta: AppErrorBase,
40
+ trpcCode: TRPC_ERROR_CODE_KEY = 'BAD_REQUEST',
41
+ ) {
42
+ super({ code: trpcCode, message: meta.message });
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Attaches typed application-error metadata to a tRPC error response.
48
+ *
49
+ * Pass this formatter to each authorization scope's context-bound `initTRPC.create()` call.
50
+ */
51
+ export const appErrorFormatter = ({
52
+ shape,
53
+ error,
54
+ }: {
55
+ shape: TRPCDefaultErrorShape;
56
+ error: TRPCError;
57
+ }): AppErrorShape => ({
58
+ ...shape,
59
+ data: {
60
+ ...shape.data,
61
+ ...(error instanceof AppError ? { appError: error.meta } : {}),
62
+ },
63
+ });
64
+
65
+ /**
66
+ * Throws a typed application error.
67
+ *
68
+ * A direct procedure error type accepts all of that procedure's variants. A whole procedure error
69
+ * map accepts only variants shared by every procedure; this is intentionally narrower than the
70
+ * client-side whole-map resolution used by `getAppError`.
71
+ */
72
+ export function throwAppError<T>(
73
+ meta: AppErrorInput<T>,
74
+ trpcCode: TRPC_ERROR_CODE_KEY = 'BAD_REQUEST',
75
+ ): never {
76
+ throw new AppError(meta as unknown as AppErrorBase, trpcCode);
77
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@prairielearn/tsconfig/tsconfig.package-node.json",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx"
5
+ }
6
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ dir: `${import.meta.dirname}/src`,
6
+ coverage: {
7
+ reporter: ['html', 'text-summary', 'cobertura'],
8
+ include: ['src/**/*.{ts,tsx}'],
9
+ },
10
+ },
11
+ });