Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createCallerFactory, createTRPCRouter } from '@src/server/api/trpc';
import { userMetadataRouter } from './routers/userMetadata';
import { storageRouter } from './routers/storage';

/**
* This is the primary router for your server.
*
* All routers added in /api/routers should be manually added here.
*/
export const appRouter = createTRPCRouter({
userMetadata: userMetadataRouter,
storage: storageRouter,
});

// export type definition of API
export type AppRouter = typeof appRouter;

export const createCaller = createCallerFactory(appRouter);
50 changes: 50 additions & 0 deletions src/server/api/routers/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { callStorageAPI, getUploadURL } from '@src/utils/storage';
import { createTRPCRouter, publicProcedure, protectedProcedure } from '../trpc';
import { z } from 'zod';
import { TRPCError } from '@trpc/server';

const getDeleteSchema = z.object({
objectID: z.string(),
});

const createUploadSchema = z.object({
objectID: z.string(),
mime: z.string(),
});
export const storageRouter = createTRPCRouter({
get: publicProcedure.input(getDeleteSchema).query(async ({ input }) => {
const data = await callStorageAPI('GET', input.objectID);
if (data.message !== 'success') {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Nebula API errored on request',
cause: data,
});
}
return data;
}),
delete: protectedProcedure.input(getDeleteSchema).query(async ({ input }) => {
const data = await callStorageAPI('DELETE', input.objectID);
if (data.message !== 'success') {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Nebula API errored on request',
cause: data,
});
}
return data;
}),
createUpload: protectedProcedure
.input(createUploadSchema)
.mutation(async ({ input }) => {
const data = await getUploadURL(input.objectID, input.mime);
if (data.message !== 'success') {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Nebula API errored on request',
cause: data,
});
}
return data;
}),
});
53 changes: 53 additions & 0 deletions src/server/api/routers/userMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { z } from 'zod';
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
import { eq, sql } from 'drizzle-orm';

import { insertUserMetadata } from '@src/server/db/models';
import { userMetadata, user as users } from '@src/server/db/schema/user';

const byIdSchema = z.object({ id: z.string().uuid() });

const updateByIdSchema = z.object({
updateUser: insertUserMetadata.omit({ id: true }),
});
const nameSchema = z.object({
name: z.string().default(''),
});

export const userMetadataRouter = createTRPCRouter({
byId: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
const { id } = input;
const userMetadata = await ctx.db.query.userMetadata.findFirst({
where: (userMetadata) => eq(userMetadata.id, id),
with: { clubs: true },
});

return userMetadata;
}),
updateById: protectedProcedure
.input(updateByIdSchema)
.mutation(async ({ input, ctx }) => {
const { updateUser } = input;
const { user } = ctx.session;

await ctx.db
.update(userMetadata)
.set(updateUser)
.where(eq(userMetadata.id, user.id));
}),
deleteById: protectedProcedure.mutation(async ({ ctx }) => {
const { user } = ctx.session;
await ctx.db.delete(users).where(eq(users.id, user.id));
await ctx.db.delete(userMetadata).where(eq(userMetadata.id, user.id));
}),
searchByName: publicProcedure
.input(nameSchema)
.query(async ({ input, ctx }) => {
const users = ctx.db.query.userMetadata.findMany({
where: sql`CONCAT(${userMetadata.firstName},' ',${
userMetadata.lastName
}) ilike ${'%' + input.name + '%'}`,
});
return await users;
}),
});
114 changes: 114 additions & 0 deletions src/server/api/trpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
* 1. You want to modify request context (see Part 1).
* 2. You want to create a new middleware or type of procedure (see Part 3).
*
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
* need to use are documented accordingly near the end.
*/

import { initTRPC, TRPCError } from '@trpc/server';
import superjson from 'superjson';
import { ZodError } from 'zod';

import { getServerAuthSession } from '@src/server/auth';
import { db } from '@src/server/db';
import { cache } from 'react';

/**
* 1. CONTEXT
*
* This section defines the "contexts" that are available in the backend API.
*
* These allow you to access things when processing a request, like the database, the session, etc.
*/

/**
* This is the actual context you will use in your router. It will be used to process every request
* that goes through your tRPC endpoint.
*
* @see https://trpc.io/docs/context
*/
export const createTRPCContext = cache(async () => {
// Fetch stuff that depends on the request
const session = await getServerAuthSession();
return {
session,
db,
};
});

export type Context = Awaited<ReturnType<typeof createTRPCContext>>;

/**
* 2. INITIALIZATION
*
* This is where the tRPC API is initialized, connecting the context and transformer. We also parse
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
* errors on the backend.
*/

const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});

/**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
*
* These are the pieces you use to build your tRPC API. You should import these a lot in the
* "/src/server/api/routers" directory.
*/

/**
* This is how you create new routers and sub-routers in your tRPC API.
*
* @see https://trpc.io/docs/router
*/
export const createTRPCRouter = t.router;

/**
* backend calls instead of proxy
*/
export const createCallerFactory = t.createCallerFactory;

/**
* Public (unauthenticated) procedure
*
* This is the base piece you use to build new queries and mutations on your tRPC API. It does not
* guarantee that a user querying is authorized, but you can still access user session data if they
* are logged in.
*/
export const publicProcedure = t.procedure;

/** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
// infers the `session` as non-nullable
session: { ...ctx.session, user: ctx.session.user },
},
});
});

/**
* Protected (authenticated) procedure
*
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
* the session is valid and guarantees `ctx.session.user` is not null.
*
* @see https://trpc.io/docs/procedures
*/
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
50 changes: 50 additions & 0 deletions src/trpc/react.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client';

import { type QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createTRPCClient, loggerLink, httpBatchLink } from '@trpc/client';
import { useState } from 'react';

import { type AppRouter } from '@src/server/api/root';
import { getUrl, makeQueryClient, transformer } from './shared';
import { createTRPCContext } from '@trpc/tanstack-react-query';

let browserQueryClient: QueryClient;

export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();

function getQueryClient() {
if (typeof window === 'undefined') {
return makeQueryClient();
} else {
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}

export function TRPCReactProvider(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();

const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === 'development' ||
(op.direction === 'down' && op.result instanceof Error),
}),
httpBatchLink({
transformer,
url: getUrl(),
}),
],
}),
);

return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{props.children}
</TRPCProvider>
</QueryClientProvider>
);
}
17 changes: 17 additions & 0 deletions src/trpc/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'server-only';

import { createTRPCContext } from '@src/server/api/trpc';
import { cache } from 'react';
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { appRouter } from '@src/server/api/root';
import { makeQueryClient } from './shared';

export const getQueryClient = cache(makeQueryClient);

export const trpc = createTRPCOptionsProxy({
ctx: createTRPCContext,
router: appRouter,
queryClient: getQueryClient,
});

export const api = appRouter.createCaller(createTRPCContext);
49 changes: 49 additions & 0 deletions src/trpc/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server';
import superjson from 'superjson';
import { type AppRouter } from '@src/server/api/root';
import {
QueryClient,
defaultShouldDehydrateQuery,
} from '@tanstack/react-query';

export const transformer = superjson;
function getBaseUrl() {
if (typeof window !== 'undefined') return '';
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
export function getUrl() {
return getBaseUrl() + '/api/trpc';
}

export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000,
},
dehydrate: {
serializeData: transformer.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === 'pending',
},
hydrate: {
deserializeData: transformer.deserialize,
},
},
});
}
/**
* Inference helper for inputs.
*
* @example type HelloInput = RouterInputs['example']['hello']
*/
export type RouterInputs = inferRouterInputs<AppRouter>;

/**
* Inference helper for outputs.
*
* @example type HelloOutput = RouterOutputs['example']['hello']
*/
export type RouterOutputs = inferRouterOutputs<AppRouter>;
Loading