Skip to content

Commit

Permalink
add api package
Browse files Browse the repository at this point in the history
  • Loading branch information
younes200 committed Sep 26, 2023
1 parent a6d5293 commit ad1e263
Show file tree
Hide file tree
Showing 18 changed files with 1,342 additions and 0 deletions.
541 changes: 541 additions & 0 deletions .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions packages/api/package.json
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"
}
}
109 changes: 109 additions & 0 deletions packages/api/src/index.ts
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();

13 changes: 13 additions & 0 deletions packages/api/src/openapi.ts
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'],
});
4 changes: 4 additions & 0 deletions packages/api/src/prisma.ts
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()
18 changes: 18 additions & 0 deletions packages/api/src/routers/index.ts
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;
141 changes: 141 additions & 0 deletions packages/api/src/routers/playlist.ts
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;
}
}),
});
Loading

0 comments on commit ad1e263

Please sign in to comment.