trpc: improve folders structure

This commit is contained in:
vchikalkin 2023-03-25 14:59:41 +03:00
parent 1f1b3afd7b
commit 23e51a6420
20 changed files with 283 additions and 144 deletions

View File

@ -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() {
// // ...
// },
});

View File

@ -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 },

View File

@ -20,7 +20,7 @@ export function common({ store, trpcClient }: ProcessContext) {
key,
});
trpcClient.quote.getData
trpcClient.getQuote
.query({
values: {
quote: quote.value,

View File

@ -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';

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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,
};
}),
});

View File

@ -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<CoreTypes.AdditionalData[Key]>;
};
export async function getRequestData({
export async function createRequestData({
context,
input,
user,

View File

@ -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;

View File

@ -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;

40
apps/web/server/trpc.ts Normal file
View File

@ -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<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;

View File

@ -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<AppRouter>({
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<AppRouter>({
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;

View File

@ -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<ResponseCalculate, 'errors'>;
return {
error: errors[0],
success: false,
};
}
}),
});
export default calculateRouter;

View File

@ -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;

View File

@ -1,7 +0,0 @@
import type { Context } from './context';
import { initTRPC } from '@trpc/server';
import SuperJSON from 'superjson';
export const t = initTRPC.context<Context>().create({
transformer: SuperJSON,
});

View File

@ -1,5 +0,0 @@
import type { trpcClient, trpcPureClient } from './client';
export type TRPCClient = typeof trpcClient;
export type TRPCPureClient = typeof trpcPureClient;