-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
1,342 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
{ | ||
"name": "@celluloid/api", | ||
"private": true, | ||
"version": "2.0.1-alpha-6", | ||
"type": "module", | ||
"scripts": { | ||
"build": "esbuild src/index.ts --bundle --packages=external --platform=node --format=esm --outdir=dist --sourcemap", | ||
"dev": "tsx watch --clear-screen=false src ", | ||
"start": "node dist", | ||
"test-start": "start-server-and-test 'node dist/index' 2021" | ||
}, | ||
"dependencies": { | ||
"@celluloid/database": "*", | ||
"@trpc/server": "^10.38.4", | ||
"bcryptjs": "^2.4.3", | ||
"change-case": "^4.1.2", | ||
"connect-redis": "^7.1.0", | ||
"cookie-parser": "^1.4.6", | ||
"cors": "^2.8.5", | ||
"dotenv": "^16.3.1", | ||
"express": "^4.17.1", | ||
"express-session": "^1.17.3", | ||
"lodash": "^4.17.21", | ||
"redis": "^4.6.9", | ||
"swagger-ui-express": "^5.0.0", | ||
"trpc-openapi": "^1.2.0", | ||
"uuid": "^9.0.1", | ||
"zod": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/express": "^4.17.17", | ||
"@types/swagger-ui-express": "^4.1.3", | ||
"@types/uuid": "^9.0.4", | ||
"esbuild": "^0.19.3", | ||
"start-server-and-test": "^2.0.0", | ||
"tsx": "^3.12.10", | ||
"wait-port": "^1.0.4" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import * as trpcExpress from '@trpc/server/adapters/express'; | ||
import bcrypt from 'bcryptjs'; | ||
import cookieParser from 'cookie-parser'; | ||
import cors from 'cors'; | ||
import * as dotEnv from 'dotenv'; | ||
import express from 'express'; | ||
import passport from 'passport'; | ||
import { Strategy as LocalStrategy } from 'passport-local'; | ||
import * as path from "path"; | ||
import swaggerUi from 'swagger-ui-express'; | ||
import { createOpenApiExpressMiddleware } from 'trpc-openapi'; | ||
|
||
import { openApiDocument } from './openapi'; | ||
import { prisma } from './prisma'; | ||
import { appRouter } from './routers'; | ||
import { createSession } from './session'; | ||
import { createContext } from './trpc'; | ||
|
||
dotEnv.config({ path: path.resolve("..", "..", ".env") }); | ||
|
||
const trpcApiEndpoint = '/trpc' | ||
|
||
|
||
async function main() { | ||
// express implementation | ||
const app = express(); | ||
|
||
// parse cookies | ||
app.use(cookieParser()); | ||
|
||
// Setup CORS | ||
app.use(cors({ | ||
// origin: 'http://localhost:3000', | ||
credentials: true, | ||
})); | ||
|
||
app.disable('x-powered-by'); | ||
|
||
app.use(express.json()); | ||
app.use(express.urlencoded({ extended: true })); | ||
app.use(createSession()); | ||
app.use(passport.authenticate("session")); | ||
|
||
|
||
passport.serializeUser((user: any, done) => { | ||
done(null, user.id) | ||
}); | ||
|
||
passport.deserializeUser(async (id: string, done) => { | ||
const user = await prisma.user.findUnique({ where: { id } }) | ||
if (user) { | ||
return done(null, user); | ||
} else { | ||
console.error( | ||
`Deserialize user failed: user with id` + ` ${id} does not exist` | ||
); | ||
return done(new Error("InvalidUser")); | ||
} | ||
}); | ||
|
||
passport.use( | ||
new LocalStrategy(async (username: string, password: string, done) => { | ||
const user = await prisma.user.findUnique({ where: { username: username } }) | ||
if (!user) { | ||
return done(new Error("InvalidUser")); | ||
} | ||
if (!bcrypt.compareSync(password, user.password)) { | ||
return done(new Error("InvalidUser")); | ||
} | ||
if (!user.confirmed && user.role !== "Student") { | ||
return done(new Error("UserNotConfirmed")); | ||
} | ||
return done(null, user); | ||
|
||
}), | ||
); | ||
|
||
app.use((req, _res, next) => { | ||
// request logger | ||
console.log('⬅️ ', req.method, req.path, req.body ?? req.query); | ||
|
||
next(); | ||
}); | ||
|
||
app.use( | ||
trpcApiEndpoint, | ||
trpcExpress.createExpressMiddleware({ | ||
router: appRouter, | ||
createContext, | ||
}), | ||
); | ||
|
||
// Handle incoming OpenAPI requests | ||
app.use('/api', createOpenApiExpressMiddleware({ router: appRouter, createContext })); | ||
|
||
|
||
// Serve Swagger UI with our OpenAPI schema | ||
app.use('/', swaggerUi.serve); | ||
app.get('/', swaggerUi.setup(openApiDocument)); | ||
|
||
|
||
|
||
app.listen(2021, () => { | ||
console.log('listening on port 2021'); | ||
}); | ||
} | ||
|
||
void main(); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { generateOpenApiDocument } from 'trpc-openapi'; | ||
|
||
import { appRouter } from './routers'; | ||
|
||
// Generate OpenAPI schema document | ||
export const openApiDocument = generateOpenApiDocument(appRouter, { | ||
title: 'Example CRUD API', | ||
description: 'OpenAPI compliant REST API built using tRPC with Express', | ||
version: '1.0.0', | ||
baseUrl: 'http://localhost:2021/api', | ||
docsUrl: 'https://github.com/jlalmes/trpc-openapi', | ||
tags: ['auth', 'users', 'projects'], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
|
||
|
||
import { PrismaClient } from '@celluloid/database/client-prisma' | ||
export const prisma = new PrismaClient() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/** | ||
* This file contains the root router of your tRPC-backend | ||
*/ | ||
import { publicProcedure, router } from '../trpc'; | ||
import { playlistRouter } from './playlist' | ||
import { projectRouter } from './project' | ||
import { userRouter } from './user'; | ||
|
||
|
||
export const appRouter = router({ | ||
healthcheck: publicProcedure.query(() => 'yay!'), | ||
|
||
project: projectRouter, | ||
user: userRouter, | ||
playlist: playlistRouter | ||
}); | ||
|
||
export type AppRouter = typeof appRouter; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { UserRole } from '@celluloid/database/client-prisma'; | ||
import { TRPCError } from '@trpc/server'; | ||
import { z } from 'zod'; | ||
|
||
import { prisma } from '../prisma'; | ||
import { protectedProcedure, publicProcedure, router } from '../trpc'; | ||
|
||
// const defaultPostSelect = Prisma.validator<Prisma.ProjectSelect>()({ | ||
// id: true, | ||
// title: true, | ||
// text: true, | ||
// createdAt: true, | ||
// updatedAt: true, | ||
// }); | ||
|
||
|
||
export const playlistRouter = router({ | ||
list: publicProcedure | ||
.input( | ||
z.object({ | ||
limit: z.number().min(1).max(100).nullish(), | ||
cursor: z.string().nullish(), | ||
authoredOnly: z.boolean().nullish().default(false), | ||
}), | ||
) | ||
.query(async ({ ctx, input }) => { | ||
/** | ||
* For pagination docs you can have a look here | ||
* @see https://trpc.io/docs/useInfiniteQuery | ||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/pagination | ||
*/ | ||
|
||
const limit = input.limit ?? 50; | ||
const { cursor } = input; | ||
|
||
const items = await prisma.playlist.findMany({ | ||
// select: defaultPostSelect, | ||
// get an extra item at the end which we'll use as next cursor | ||
take: limit + 1, | ||
where: { | ||
userId: input.authoredOnly && ctx.user ? ctx.user.id : undefined, | ||
}, | ||
include: { | ||
|
||
}, | ||
cursor: cursor | ||
? { | ||
id: cursor, | ||
} | ||
: undefined, | ||
orderBy: { | ||
publishedAt: 'desc', | ||
}, | ||
}); | ||
let nextCursor: typeof cursor | undefined = undefined; | ||
if (items.length > limit) { | ||
// Remove the last item and use it as next cursor | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
const nextItem = items.pop()!; | ||
nextCursor = nextItem.id; | ||
} | ||
|
||
return { | ||
items: items.reverse(), | ||
nextCursor, | ||
}; | ||
}), | ||
byId: publicProcedure | ||
.input( | ||
z.object({ | ||
id: z.string(), | ||
}), | ||
) | ||
.query(async ({ input }) => { | ||
const { id } = input; | ||
const project = await prisma.playlist.findUnique({ | ||
where: { id }, | ||
// select: defaultPostSelect, | ||
}); | ||
if (!project) { | ||
throw new TRPCError({ | ||
code: 'NOT_FOUND', | ||
message: `No playlist with id '${id}'`, | ||
}); | ||
} | ||
return project; | ||
}), | ||
add: protectedProcedure | ||
.input( | ||
z.object({ | ||
title: z.string().min(1), | ||
description: z.string(), | ||
projects: z.array(z.object({ | ||
videoId: z.string(), | ||
host: z.string(), | ||
})), | ||
objective: z.string(), | ||
levelStart: z.number(), | ||
levelEnd: z.number(), | ||
public: z.boolean(), | ||
collaborative: z.boolean(), | ||
shared: z.boolean(), | ||
userId: z.string(), | ||
}), | ||
) | ||
.mutation(async ({ input, ctx }) => { | ||
if (ctx.requirePermission(UserRole.Teacher)) { | ||
const project = await prisma.playlist.create({ | ||
select: { | ||
projects: true, | ||
}, | ||
data: { | ||
userId: ctx.user?.id, | ||
title: input.title, | ||
description: input.description, | ||
projects: { | ||
createMany: { | ||
data: input.projects.map(p => ({ | ||
videoId: p.videoId, | ||
host: p.host, | ||
title: input.title, | ||
description: input.description, | ||
objective: input.objective, | ||
levelStart: input.levelStart, | ||
levelEnd: input.levelEnd, | ||
public: input.public, | ||
collaborative: input.collaborative, | ||
shared: input.shared, | ||
userId: ctx.user?.id, | ||
})) | ||
} | ||
} | ||
|
||
}, | ||
// select: defaultPostSelect, | ||
}); | ||
return project; | ||
} | ||
}), | ||
}); |
Oops, something went wrong.