From 23e51a642027e298c507d257d39793d95a50caf4 Mon Sep 17 00:00:00 2001 From: vchikalkin Date: Sat, 25 Mar 2023 14:59:41 +0300 Subject: [PATCH] trpc: improve folders structure --- apps/web/pages/api/trpc/[trpc].ts | 33 ++++- apps/web/process/calculate/action.js | 2 +- apps/web/process/load-kp/reactions.ts | 2 +- apps/web/process/types.ts | 2 +- apps/web/{trpc => server}/context.ts | 0 apps/web/{trpc => server}/middleware.ts | 7 +- apps/web/server/procedure.ts | 12 ++ apps/web/server/routers/_app.ts | 7 ++ apps/web/server/routers/calculate/index.ts | 73 +++++++++++ .../routers/calculate/lib}/request.ts | 4 +- .../routers/calculate/lib/transform.ts} | 7 +- .../routers/calculate/lib}/validation.ts | 0 .../routers/calculate/types.ts | 0 apps/web/{trpc => server}/routers/quote.ts | 10 +- apps/web/server/trpc.ts | 40 ++++++ apps/web/trpc/client.ts | 118 +++++++++++++++--- apps/web/trpc/routers/calculate/index.ts | 86 ------------- apps/web/trpc/routers/index.ts | 12 -- apps/web/trpc/server.ts | 7 -- apps/web/trpc/types.ts | 5 - 20 files changed, 283 insertions(+), 144 deletions(-) rename apps/web/{trpc => server}/context.ts (100%) rename apps/web/{trpc => server}/middleware.ts (71%) create mode 100644 apps/web/server/procedure.ts create mode 100644 apps/web/server/routers/_app.ts create mode 100644 apps/web/server/routers/calculate/index.ts rename apps/web/{trpc/routers/calculate => server/routers/calculate/lib}/request.ts (99%) rename apps/web/{trpc/routers/calculate/convert.ts => server/routers/calculate/lib/transform.ts} (95%) rename apps/web/{trpc/routers/calculate => server/routers/calculate/lib}/validation.ts (100%) rename apps/web/{trpc => server}/routers/calculate/types.ts (100%) rename apps/web/{trpc => server}/routers/quote.ts (92%) create mode 100644 apps/web/server/trpc.ts delete mode 100644 apps/web/trpc/routers/calculate/index.ts delete mode 100644 apps/web/trpc/routers/index.ts delete mode 100644 apps/web/trpc/server.ts delete mode 100644 apps/web/trpc/types.ts diff --git a/apps/web/pages/api/trpc/[trpc].ts b/apps/web/pages/api/trpc/[trpc].ts index 3b8d088..b5455b5 100644 --- a/apps/web/pages/api/trpc/[trpc].ts +++ b/apps/web/pages/api/trpc/[trpc].ts @@ -1,9 +1,38 @@ +/* eslint-disable jsdoc/check-tag-names */ /* eslint-disable canonical/filename-match-regex */ -import { createContext } from '@/trpc/context'; -import appRouter from '@/trpc/routers'; +import { createContext } from '@/server/context'; +import { appRouter } from '@/server/routers/_app'; import * as trpcNext from '@trpc/server/adapters/next'; export default trpcNext.createNextApiHandler({ + /** + * Enable query batching + */ + batching: { + enabled: true, + }, + + /** + * @link https://trpc.io/docs/context + */ createContext, + + /** + * @link https://trpc.io/docs/error-handling + */ + onError({ error }) { + if (error.code === 'INTERNAL_SERVER_ERROR') { + // send to bug reporting + // eslint-disable-next-line no-console + console.error('Something went wrong', error); + } + }, + router: appRouter, + /** + * @link https://trpc.io/docs/caching#api-response-caching + */ + // responseMeta() { + // // ... + // }, }); diff --git a/apps/web/process/calculate/action.js b/apps/web/process/calculate/action.js index 196b592..68a63e8 100644 --- a/apps/web/process/calculate/action.js +++ b/apps/web/process/calculate/action.js @@ -25,7 +25,7 @@ export async function action({ store, trpcClient }) { const payments = toJS($tables.payments.values); - trpcClient.calculate.calculate + trpcClient.calculate .mutate({ insurance: { values: insurance }, payments: { values: payments }, diff --git a/apps/web/process/load-kp/reactions.ts b/apps/web/process/load-kp/reactions.ts index a495318..56519d7 100644 --- a/apps/web/process/load-kp/reactions.ts +++ b/apps/web/process/load-kp/reactions.ts @@ -20,7 +20,7 @@ export function common({ store, trpcClient }: ProcessContext) { key, }); - trpcClient.quote.getData + trpcClient.getQuote .query({ values: { quote: quote.value, diff --git a/apps/web/process/types.ts b/apps/web/process/types.ts index 422be7f..b13166d 100644 --- a/apps/web/process/types.ts +++ b/apps/web/process/types.ts @@ -1,6 +1,6 @@ import type { User } from '@/api/user/types'; import type RootStore from '@/stores/root'; -import type { TRPCPureClient } from '@/trpc/types'; +import type { TRPCPureClient } from '@/trpc/client'; import type { ApolloClient } from '@apollo/client'; import type { QueryClient } from '@tanstack/react-query'; diff --git a/apps/web/trpc/context.ts b/apps/web/server/context.ts similarity index 100% rename from apps/web/trpc/context.ts rename to apps/web/server/context.ts diff --git a/apps/web/trpc/middleware.ts b/apps/web/server/middleware.ts similarity index 71% rename from apps/web/trpc/middleware.ts rename to apps/web/server/middleware.ts index c1d1b05..553e146 100644 --- a/apps/web/trpc/middleware.ts +++ b/apps/web/server/middleware.ts @@ -1,6 +1,9 @@ -import { t } from './server'; +import { t } from './trpc'; import { TRPCError } from '@trpc/server'; +/** + * @see https://trpc.io/docs/v10/middlewares + */ export const userMiddleware = t.middleware(({ ctx, next }) => { if (process.env.NODE_ENV !== 'development' && !ctx.user) { throw new TRPCError({ @@ -14,3 +17,5 @@ export const userMiddleware = t.middleware(({ ctx, next }) => { }, }); }); + +export const middleware = t.middleware; diff --git a/apps/web/server/procedure.ts b/apps/web/server/procedure.ts new file mode 100644 index 0000000..dba8832 --- /dev/null +++ b/apps/web/server/procedure.ts @@ -0,0 +1,12 @@ +import { userMiddleware } from './middleware'; +import { t } from './trpc'; + +/** + * Create an unprotected procedure + * + * @see https://trpc.io/docs/v10/procedures + */ + +export const publicProcedure = t.procedure; + +export const protectedProcedure = t.procedure.use(userMiddleware); diff --git a/apps/web/server/routers/_app.ts b/apps/web/server/routers/_app.ts new file mode 100644 index 0000000..2100bc5 --- /dev/null +++ b/apps/web/server/routers/_app.ts @@ -0,0 +1,7 @@ +import { mergeRouters } from '../trpc'; +import { calculateRouter } from './calculate'; +import { quoteRouter } from './quote'; + +export const appRouter = mergeRouters(quoteRouter, calculateRouter); + +export type AppRouter = typeof appRouter; diff --git a/apps/web/server/routers/calculate/index.ts b/apps/web/server/routers/calculate/index.ts new file mode 100644 index 0000000..9b85a89 --- /dev/null +++ b/apps/web/server/routers/calculate/index.ts @@ -0,0 +1,73 @@ +import { router } from '../../trpc'; +import { createRequestData } from './lib/request'; +import { transformCalculateResults } from './lib/transform'; +import { validate } from './lib/validation'; +import { CalculateInputSchema, CalculateOutputSchema } from './types'; +import { calculate } from '@/api/core/query'; +import initializeApollo from '@/apollo/client'; +import { STALE_TIME } from '@/constants/request'; +import { protectedProcedure } from '@/server/procedure'; +import type { QueryFunctionContext } from '@tanstack/react-query'; +import { QueryClient } from '@tanstack/react-query'; + +export const calculateRouter = router({ + calculate: protectedProcedure + .input(CalculateInputSchema) + .output(CalculateOutputSchema) + .mutation(async ({ input, ctx }) => { + const apolloClient = initializeApollo(); + const queryClient = new QueryClient(); + + const validationResult = await validate({ + context: { + apolloClient, + queryClient, + user: ctx.user, + }, + input, + }); + + if (validationResult.success === false) { + return { + error: validationResult.error, + success: false, + }; + } + + const requestData = await createRequestData({ + context: { + apolloClient, + queryClient, + user: ctx.user, + }, + input, + user: ctx.user, + }); + + const calculateResult = await queryClient.fetchQuery( + ['calculate'], + (context: QueryFunctionContext) => calculate(requestData, context), + { + staleTime: STALE_TIME, + } + ); + + if (calculateResult.errors?.length > 0) { + return { + error: calculateResult.errors[0], + success: false, + }; + } + + const result = transformCalculateResults({ + calculateInput: input, + requestCalculate: requestData, + responseCalculate: calculateResult, + }); + + return { + data: result, + success: true, + }; + }), +}); diff --git a/apps/web/trpc/routers/calculate/request.ts b/apps/web/server/routers/calculate/lib/request.ts similarity index 99% rename from apps/web/trpc/routers/calculate/request.ts rename to apps/web/server/routers/calculate/lib/request.ts index 4e82c50..afb9141 100644 --- a/apps/web/trpc/routers/calculate/request.ts +++ b/apps/web/server/routers/calculate/lib/request.ts @@ -1,5 +1,5 @@ /* eslint-disable sonarjs/cognitive-complexity */ -import type { CalculateInput, Context } from './types'; +import type { CalculateInput, Context } from '../types'; import type * as CoreTypes from '@/api/core/types'; import type { User } from '@/api/user/types'; import { ESN, NSIB_MAX, VAT } from '@/constants/values'; @@ -26,7 +26,7 @@ type AdditionalDataGetters = { [Key in keyof CoreTypes.AdditionalData]: () => Promise; }; -export async function getRequestData({ +export async function createRequestData({ context, input, user, diff --git a/apps/web/trpc/routers/calculate/convert.ts b/apps/web/server/routers/calculate/lib/transform.ts similarity index 95% rename from apps/web/trpc/routers/calculate/convert.ts rename to apps/web/server/routers/calculate/lib/transform.ts index baba5ba..831302e 100644 --- a/apps/web/trpc/routers/calculate/convert.ts +++ b/apps/web/server/routers/calculate/lib/transform.ts @@ -1,4 +1,4 @@ -import type { CalculateInput, OutputData } from './types'; +import type { CalculateInput, OutputData } from '../types'; import type { RequestCalculate, ResponseCalculate } from '@/api/core/types'; import { ESN, NDFL, VAT } from '@/constants/values'; import { last } from 'radash'; @@ -9,7 +9,10 @@ type Input = { responseCalculate: ResponseCalculate; }; -export function convertCalculateResults({ responseCalculate, calculateInput }: Input): OutputData { +export function transformCalculateResults({ + responseCalculate, + calculateInput, +}: Input): OutputData { const { values: inputValues } = calculateInput; const { postValues, columns, preparedValues } = responseCalculate; diff --git a/apps/web/trpc/routers/calculate/validation.ts b/apps/web/server/routers/calculate/lib/validation.ts similarity index 100% rename from apps/web/trpc/routers/calculate/validation.ts rename to apps/web/server/routers/calculate/lib/validation.ts diff --git a/apps/web/trpc/routers/calculate/types.ts b/apps/web/server/routers/calculate/types.ts similarity index 100% rename from apps/web/trpc/routers/calculate/types.ts rename to apps/web/server/routers/calculate/types.ts diff --git a/apps/web/trpc/routers/quote.ts b/apps/web/server/routers/quote.ts similarity index 92% rename from apps/web/trpc/routers/quote.ts rename to apps/web/server/routers/quote.ts index 69d9c5b..706b946 100644 --- a/apps/web/trpc/routers/quote.ts +++ b/apps/web/server/routers/quote.ts @@ -1,6 +1,6 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable canonical/sort-keys */ -import { t } from '../server'; +import { publicProcedure } from '../procedure'; +import { router } from '../trpc'; import defaultValues from '@/config/default-values'; import * as insuranceTable from '@/config/tables/insurance-table'; import * as addProduct from '@/process/add-product'; @@ -30,8 +30,8 @@ const defaultInsurance = { const defaultFingap = { keys: [] }; const defaultPayments = { values: [] }; -const quoteRouter = t.router({ - getData: t.procedure +export const quoteRouter = router({ + getQuote: publicProcedure .input(GetQuoteInputDataSchema) .output(GetQuoteOutputDataSchema) .query(async ({ input }) => { @@ -67,5 +67,3 @@ const quoteRouter = t.router({ }; }), }); - -export default quoteRouter; diff --git a/apps/web/server/trpc.ts b/apps/web/server/trpc.ts new file mode 100644 index 0000000..ed449a8 --- /dev/null +++ b/apps/web/server/trpc.ts @@ -0,0 +1,40 @@ +/** + * This is your entry point to setup the root configuration for tRPC on the server. + * - `initTRPC` should only be used once per app. + * - We export only the functionality that we use so we can enforce which base procedures should be used + * + * Learn how to create protected base procedures and other things below: + * + * @see https://trpc.io/docs/v10/router + * @see https://trpc.io/docs/v10/procedures + */ + +import type { Context } from './context'; +import { initTRPC } from '@trpc/server'; +import SuperJSON from 'superjson'; + +export const t = initTRPC.context().create({ + /** + * @see https://trpc.io/docs/v10/error-formatting + */ + errorFormatter({ shape }) { + return shape; + }, + + /** + * @see https://trpc.io/docs/v10/data-transformers + */ + transformer: SuperJSON, +}); + +/** + * Create a router + * + * @see https://trpc.io/docs/v10/router + */ +export const router = t.router; + +/** + * @see https://trpc.io/docs/v10/merging-routers + */ +export const mergeRouters = t.mergeRouters; diff --git a/apps/web/trpc/client.ts b/apps/web/trpc/client.ts index b7ccc53..b54a096 100644 --- a/apps/web/trpc/client.ts +++ b/apps/web/trpc/client.ts @@ -1,10 +1,24 @@ -import type { AppRouter } from './routers'; import getUrls from '@/config/urls'; -import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; +import type { AppRouter } from '@/server/routers/_app'; +import { createTRPCProxyClient, httpBatchLink, loggerLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; +import type { NextPageContext } from 'next'; import SuperJSON from 'superjson'; import { isServer } from 'tools/common'; +export type SSRContext = NextPageContext & { + /** + * Set HTTP Status code + * + * @example + * const utils = trpc.useContext(); + * if (utils.ssrContext) { + * utils.ssrContext.status = 404; + * } + */ + status?: number; +}; + const { BASE_PATH, PORT } = getUrls(); function getBaseUrl() { @@ -13,40 +27,108 @@ function getBaseUrl() { return `http://localhost:${PORT ?? 3000}${BASE_PATH}`; } -const url = `${getBaseUrl()}/api/trpc`; - export const trpcClient = createTRPCNext({ config({ ctx }) { + /** + * If you want to use SSR, you need to use the server's full URL + * + * @link https://trpc.io/docs/ssr + */ return { + /** + * @link https://trpc.io/docs/links + */ links: [ + // adds pretty logs to your console in development and logs errors in production + loggerLink({ + enabled: (opts) => + process.env.NODE_ENV === 'development' || + (opts.direction === 'down' && opts.result instanceof Error), + }), httpBatchLink({ + /** + * Set custom request headers on every request from tRPC + * + * @link https://trpc.io/docs/ssr + */ headers() { - return { - cookie: ctx?.req?.headers.cookie, - }; + if (!ctx?.req?.headers) { + return {}; + } + // To use SSR properly, you need to forward the client's headers to the server + // This is so you can pass through things like cookies when we're server-side rendering + + const { + // If you're using Node 18 before 18.15.0, omit the "connection" header + connection: _connection, + ...headers + } = ctx.req.headers; + + return headers; }, - url, + + url: `${getBaseUrl()}/api/trpc`, }), ], - queryClientConfig: { - defaultOptions: { - queries: { - refetchOnMount: false, - refetchOnWindowFocus: false, - }, - }, - }, + + /** + * @link https://trpc.io/docs/data-transformers + */ transformer: SuperJSON, + /** + * @link https://react-query.tanstack.com/reference/QueryClient + */ + // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } }, }; }, - ssr: false, + + /** + * Set headers or status code when doing SSR + */ + responseMeta(opts) { + const ctx = opts.ctx as SSRContext; + + if (ctx.status) { + // If HTTP status set, propagate that + return { + status: ctx.status, + }; + } + + const error = opts.clientErrors[0]; + if (error) { + // Propagate http first error from API calls + return { + status: error.data?.httpStatus ?? 500, + }; + } + + // for app caching with SSR see https://trpc.io/docs/caching + + return {}; + }, + + /** + * @link https://trpc.io/docs/ssr + */ + ssr: true, }); +export type TRPCClient = typeof trpcClient; + export const trpcPureClient = createTRPCProxyClient({ links: [ + // adds pretty logs to your console in development and logs errors in production + loggerLink({ + enabled: (opts) => + process.env.NODE_ENV === 'development' || + (opts.direction === 'down' && opts.result instanceof Error), + }), httpBatchLink({ - url, + url: `${getBaseUrl()}/api/trpc`, }), ], transformer: SuperJSON, }); + +export type TRPCPureClient = typeof trpcPureClient; diff --git a/apps/web/trpc/routers/calculate/index.ts b/apps/web/trpc/routers/calculate/index.ts deleted file mode 100644 index ccafd62..0000000 --- a/apps/web/trpc/routers/calculate/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { userMiddleware } from '../../middleware'; -import { t } from '../../server'; -import { convertCalculateResults } from './convert'; -import { getRequestData } from './request'; -import { CalculateInputSchema, CalculateOutputSchema } from './types'; -import { validate } from './validation'; -import { calculate } from '@/api/core/query'; -import type { ResponseCalculate } from '@/api/core/types/calculate'; -import initializeApollo from '@/apollo/client'; -import { STALE_TIME } from '@/constants/request'; -import type { QueryFunctionContext } from '@tanstack/react-query'; -import { QueryClient } from '@tanstack/react-query'; - -const calculateRouter = t.router({ - calculate: t.procedure - .use(userMiddleware) - .input(CalculateInputSchema) - .output(CalculateOutputSchema) - .mutation(async ({ input, ctx }) => { - const apolloClient = initializeApollo(); - const queryClient = new QueryClient(); - - const validationResult = await validate({ - context: { - apolloClient, - queryClient, - user: ctx.user, - }, - input, - }); - - if (validationResult.success === false) { - return { - error: validationResult.error, - success: false, - }; - } - - const payload = await getRequestData({ - context: { - apolloClient, - queryClient, - user: ctx.user, - }, - input, - user: ctx.user, - }); - - try { - const calculateResult = await queryClient.fetchQuery( - ['calculate'], - (context: QueryFunctionContext) => calculate(payload, context), - { - staleTime: STALE_TIME, - } - ); - - if (calculateResult.errors?.length > 0) { - return { - error: calculateResult.errors[0], - success: false, - }; - } - - const result = convertCalculateResults({ - calculateInput: input, - requestCalculate: payload, - responseCalculate: calculateResult, - }); - - return { - data: result, - success: true, - }; - } catch (error) { - const { errors } = error as Pick; - - return { - error: errors[0], - success: false, - }; - } - }), -}); - -export default calculateRouter; diff --git a/apps/web/trpc/routers/index.ts b/apps/web/trpc/routers/index.ts deleted file mode 100644 index 9b08557..0000000 --- a/apps/web/trpc/routers/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { t } from '../server'; -import calculateRouter from './calculate'; -import quoteRouter from './quote'; - -const appRouter = t.router({ - calculate: calculateRouter, - quote: quoteRouter, -}); - -export type AppRouter = typeof appRouter; - -export default appRouter; diff --git a/apps/web/trpc/server.ts b/apps/web/trpc/server.ts deleted file mode 100644 index b11f129..0000000 --- a/apps/web/trpc/server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Context } from './context'; -import { initTRPC } from '@trpc/server'; -import SuperJSON from 'superjson'; - -export const t = initTRPC.context().create({ - transformer: SuperJSON, -}); diff --git a/apps/web/trpc/types.ts b/apps/web/trpc/types.ts deleted file mode 100644 index b52d155..0000000 --- a/apps/web/trpc/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { trpcClient, trpcPureClient } from './client'; - -export type TRPCClient = typeof trpcClient; - -export type TRPCPureClient = typeof trpcPureClient;