Skip to content

Commit

Permalink
Merge pull request #3 from aritra1999/feature/api
Browse files Browse the repository at this point in the history
feat: api
  • Loading branch information
aritra1999 authored Nov 26, 2024
2 parents 385e4d2 + 812bc86 commit f68cf8b
Show file tree
Hide file tree
Showing 24 changed files with 4,541 additions and 1,246 deletions.
3,800 changes: 2,564 additions & 1,236 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,21 @@
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"test:unit": "vitest",
"test": "npm run test:unit -- --run"
"test": "npm run test:unit -- --run",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/eslint": "^9.6.0",
"@types/pg": "^8.11.10",
"autoprefixer": "^10.4.20",
"bits-ui": "^1.0.0-next.64",
"clsx": "^2.1.1",
"drizzle-kit": "^0.25.0",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
Expand All @@ -43,10 +48,15 @@
},
"dependencies": {
"@auth/sveltekit": "^1.7.4",
"@hono/auth-js": "^1.0.14",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"mode-watcher": "^0.5.0"
"drizzle-orm": "^0.34.1",
"drizzle-zod": "^0.5.1",
"hono": "^4.6.4",
"mode-watcher": "^0.5.0",
"pg": "^8.13.1"
}
}
35 changes: 35 additions & 0 deletions src/lib/api/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { initAuthConfig, type AuthConfig } from '@hono/auth-js';
import GitHub from '@auth/sveltekit/providers/github';
import {
SECRET_AUTH_GITHUB_ID,
SECRET_AUTH_GITHUB_SECRET,
SECRET_AUTH_SECRET
} from '$env/static/private';
import { userRouter } from './user/user.controller';
import { projectsRouter } from './projects/projects.controller';
import { websitesRouter } from './websites/websites.controller';
import { statusRouter } from './status/status.controller';

export const api = new Hono().basePath('/api');

api.use(initAuthConfig(getAuthConfig));
api.use(logger());

api.route('/user', userRouter);
api.route('/projects', projectsRouter);
api.route('/websites', websitesRouter);
api.route('/status', statusRouter);

function getAuthConfig(): AuthConfig {
return {
secret: SECRET_AUTH_SECRET,
providers: [
GitHub({
clientId: SECRET_AUTH_GITHUB_ID,
clientSecret: SECRET_AUTH_GITHUB_SECRET
})
]
};
}
25 changes: 25 additions & 0 deletions src/lib/api/middlewares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Context, Next } from 'hono';
import { z } from 'zod';

export const validateRequestBody = <T extends z.ZodType>(schema: T) => {
return async (context: Context, next: Next) => {
try {
const body = await context.req.json();
const valid = schema.safeParse(body);

if (valid.error) {
const errorResponse = {
error: 'Invalid input',
details: valid.error.errors.map((issue) => `${issue.path.join('.')} is ${issue.message}`)
};
return context.json(errorResponse, 400);
}

context.set('requestBody', valid.data);
await next();
} catch (error) {
console.log(error);
return context.json({ error: 'Invalid input json' }, 400);
}
};
};
102 changes: 102 additions & 0 deletions src/lib/api/projects/projects.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Hono, type Context } from 'hono';
import { verifyAuth } from '@hono/auth-js';
import {
createProject,
deleteProject,
getAllProjects,
getProjectBySlug,
updateProject
} from './projects.service';
import { InsertProjectSchema } from '$lib/db/schema';
import { validateRequestBody } from '../middlewares';

export const projectsRouter = new Hono();

export const getProjectsController = async (context: Context) => {
const { token } = context.get('authUser');
if (!token) return context.status(401);

const projects = await getAllProjects(String(token.id));

return context.json({ projects });
};

export const getProjectController = async (context: Context) => {
const { token } = context.get('authUser');
if (!token) return context.status(401);

const { slug } = context.req.param();
if (!slug) return context.json({ error: 'Invalid slug' }, 400);

const project = await getProjectBySlug(String(token.id), slug);
if (!project) return context.json({ error: 'Project not found' }, 404);

return context.json(project);
};

export const postProjectController = async (context: Context) => {
const { token } = context.get('authUser');
if (!token) return context.status(401);

const requestBody = context.get('requestBody');

return await createProject({
userId: token.id as string,
name: requestBody.name as string,
description: requestBody.description as string,
slug: requestBody.slug as string
})
.then((response) => {
if (response.error) {
return context.json({ error: response.error }, response.status);
}

return context.json(response.data, response.status);
})
.catch((error) => {
return context.json({ error }, 400);
});
};

export const putProjectController = async (context: Context) => {
const { token } = context.get('authUser');
if (!token) return context.status(401);

const { slug } = context.req.param();
if (!slug) return context.json({ error: 'Invalid slug' }, 400);

const requestBody = context.get('requestBody');
const updatedProject = await updateProject(String(token.id), slug, requestBody);

return context.json(
updatedProject.data ?? { error: updatedProject.error },
updatedProject.status
);
};

export const deleteProjectController = async (context: Context) => {
const { token } = context.get('authUser');
if (!token) return context.status(401);

const { slug } = context.req.param();
if (!slug) return context.json({ error: 'Invalid slug' }, 400);

const deletedProject = await deleteProject(String(token.id), slug);
return context.json(
deletedProject.data ?? { error: deletedProject.error },
deletedProject.status
);
};

const PartialInsertProjectSchema = InsertProjectSchema.pick({
name: true,
description: true,
slug: true
});

projectsRouter.use(verifyAuth());
projectsRouter.get('/:slug', getProjectController);
projectsRouter.get('/', getProjectsController);
projectsRouter.post('/', validateRequestBody(PartialInsertProjectSchema), postProjectController);
projectsRouter.put('/:slug', validateRequestBody(PartialInsertProjectSchema), putProjectController);
projectsRouter.delete('/:slug', deleteProjectController);
141 changes: 141 additions & 0 deletions src/lib/api/projects/projects.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { db } from '$lib/db/drizzle';
import { and, count, eq } from 'drizzle-orm';
import { projects, type InsertProject, type SelectProjectPartial } from '$lib/db/schema';
import { isUserPro } from '../user/user.service';
import type { ServiceResponse } from '../types';
import { prettifyErrors } from '$lib/db/utils';
import type { StatusCode } from 'hono/utils/http-status';

export const canUserCreateProject = async (userId: string): Promise<boolean> => {
if (await isUserPro(userId)) return true;

const projectsCount = await db
.select({ count: count(projects.id) })
.from(projects)
.where(eq(projects.userId, userId));

return projectsCount[0].count < 2;
};

export const getAllProjects = async (userId: string): Promise<SelectProjectPartial[]> => {
return db
.select({
id: projects.id,
name: projects.name,
description: projects.description,
slug: projects.slug
})
.from(projects)
.where(eq(projects.userId, userId));
};

export const getProjectBySlug = async (
userId: string,
projectSlug: string
): Promise<SelectProjectPartial | null> => {
const projectResponse = await db
.select({
id: projects.id,
name: projects.name,
description: projects.description,
slug: projects.slug
})
.from(projects)
.where(and(eq(projects.userId, userId), eq(projects.slug, projectSlug)))
.limit(1);

return projectResponse[0];
};

export const createProject = async (
project: InsertProject
): Promise<ServiceResponse<SelectProjectPartial>> => {
if (!(await canUserCreateProject(project.userId))) {
return {
status: 403,
error: 'Hobby users can create 2 projects!'
};
}

try {
const result = await db.insert(projects).values(project).returning({
id: projects.id,
name: projects.name,
description: projects.description,
slug: projects.slug
});
return {
status: 200,
data: result[0]
};
} catch (err) {
return {
status: 400,
error: prettifyErrors(err as Error)
};
}
};

export const updateProject = async (
userId: string,
slug: string,
updatedProjectInput: InsertProject
): Promise<ServiceResponse<SelectProjectPartial>> => {
const project = await getProjectBySlug(userId, slug);

if (!project)
return {
status: 404,
error: 'Project not found'
};

return await db
.update(projects)
.set(updatedProjectInput)
.where(and(eq(projects.userId, userId), eq(projects.slug, slug)))
.returning({
id: projects.id,
name: projects.name,
description: projects.description,
slug: projects.slug
})
.then((response) => {
return {
status: 200 as StatusCode,
data: response[0]
};
})
.catch((error) => {
return {
status: 400 as StatusCode,
error: error.message
};
});
};

export const deleteProject = async (
userId: string,
slug: string
): Promise<ServiceResponse<{ deletedId: string }>> => {
return await db
.delete(projects)
.where(and(eq(projects.userId, userId), eq(projects.slug, slug)))
.returning({ deletedId: projects.id })
.then((response) => {
if (response.length === 0)
return {
status: 404 as StatusCode,
error: 'Project not found'
};
return {
status: 200 as StatusCode,
data: response[0]
};
})
.catch((error) => {
return {
status: 400 as StatusCode,
error: error.message
};
});
};
44 changes: 44 additions & 0 deletions src/lib/api/status/status.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Hono, type Context } from 'hono';
import { getStatus, getStatusByProject, getStatusPage } from './status.service';

export const statusRouter = new Hono();

export const getStatusController = async (context: Context) => {
const { websiteId } = context.req.param();
if (!websiteId) return context.json({ error: 'Missing website ID' }, 400);

const websiteResponse = await getStatus(websiteId);

return context.json(
websiteResponse.error ? { error: websiteResponse.error } : websiteResponse.data,
websiteResponse.status
);
};

export const getStatusByProjectController = async (context: Context) => {
const { projectSlug } = context.req.param();
if (!projectSlug) return context.json({ error: 'Missing project slug' }, 400);

const websiteResponse = await getStatusByProject(projectSlug);

return context.json(
websiteResponse.error ? { error: websiteResponse.error } : websiteResponse.data,
websiteResponse.status
);
};

export const getStatusPageController = async (context: Context) => {
const { projectId } = context.req.param();
if (!projectId) return context.json({ error: 'Missing project ID' }, 400);

const websiteResponse = await getStatusPage(projectId);

return context.json(
websiteResponse.error ? { error: websiteResponse.error } : websiteResponse.data,
websiteResponse.status
);
};

statusRouter.get('/page/:projectId', getStatusPageController);
statusRouter.get('/project/:projectSlug', getStatusByProjectController);
statusRouter.get('/:websiteId', getStatusController);
Loading

0 comments on commit f68cf8b

Please sign in to comment.