init
This commit is contained in:
3
backend/src/README.md
Normal file
3
backend/src/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
bun db:generate # generates SQL migration files in /drizzle
|
||||
bun db:migrate # applies them to support.db
|
||||
bun db:studio # opens a browser UI to inspect the DB
|
||||
38
backend/src/adapters/sqlite.ts
Normal file
38
backend/src/adapters/sqlite.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { db } from '../db/index.ts'
|
||||
import { tickets } from '../db/schema.ts'
|
||||
import type { Ticket, StorageAdapter } from '../types.ts'
|
||||
|
||||
export class SQLiteAdapter implements StorageAdapter {
|
||||
getTickets(): Ticket[] {
|
||||
return db.select().from(tickets).all()
|
||||
}
|
||||
|
||||
getTicket(id: string): Ticket | null {
|
||||
return db.select().from(tickets).where(eq(tickets.id, id)).get() ?? null
|
||||
}
|
||||
|
||||
createTicket({ subject, description, type }: Pick<Ticket, 'subject' | 'description' | 'type'>): Ticket {
|
||||
const ticket: Ticket = {
|
||||
id: crypto.randomUUID(),
|
||||
subject,
|
||||
description,
|
||||
status: 'open',
|
||||
type,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
db.insert(tickets).values(ticket).run()
|
||||
return ticket
|
||||
}
|
||||
|
||||
updateTicket(id: string, patch: Partial<Ticket>): Ticket | null {
|
||||
const current = this.getTicket(id)
|
||||
if (!current) return null
|
||||
db.update(tickets).set(patch).where(eq(tickets.id, id)).run()
|
||||
return { ...current, ...patch }
|
||||
}
|
||||
|
||||
deleteTicket(id: string): void {
|
||||
db.delete(tickets).where(eq(tickets.id, id)).run()
|
||||
}
|
||||
}
|
||||
6
backend/src/db/index.ts
Normal file
6
backend/src/db/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Database } from 'bun:sqlite'
|
||||
import { drizzle } from 'drizzle-orm/bun-sqlite'
|
||||
import { tickets } from './schema.ts'
|
||||
|
||||
const sqlite = new Database('app.db')
|
||||
export const db = drizzle(sqlite, { schema: { tickets } })
|
||||
25
backend/src/db/schema.ts
Normal file
25
backend/src/db/schema.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { int, text, sqliteTable } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const tickets = sqliteTable("tickets", {
|
||||
id: text("id").primaryKey(),
|
||||
subject: text("subject").notNull(),
|
||||
description: text("description").notNull().default(""),
|
||||
type: text("type", {
|
||||
enum: [
|
||||
"bug",
|
||||
"billing",
|
||||
"account",
|
||||
"feature-request",
|
||||
"feedback",
|
||||
"other",
|
||||
],
|
||||
})
|
||||
.notNull()
|
||||
.default("other"),
|
||||
status: text("status", {
|
||||
enum: ["open", "in-progress", "resolved", "closed"],
|
||||
})
|
||||
.notNull()
|
||||
.default("open"),
|
||||
createdAt: text("createdAt").notNull(),
|
||||
});
|
||||
19
backend/src/index.ts
Normal file
19
backend/src/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Fastify from 'fastify'
|
||||
import cors from '@fastify/cors'
|
||||
import { authMiddleware } from './middleware/auth.ts'
|
||||
import { storageMiddleware } from './middleware/storage.ts'
|
||||
import { storageModeRouter } from './routes/storageMode.ts'
|
||||
import { ticketsRouter } from './routes/tickets.ts'
|
||||
|
||||
const app = Fastify({ logger: true })
|
||||
const PORT = Number(process.env.PORT) || 3000
|
||||
|
||||
await app.register(cors)
|
||||
await app.register(authMiddleware)
|
||||
await app.register(storageMiddleware)
|
||||
await app.register(storageModeRouter, { prefix: '/api/storage-mode' })
|
||||
await app.register(ticketsRouter, { prefix: '/api/tickets' })
|
||||
|
||||
app.listen({ port: PORT }, () => {
|
||||
console.log(`Backend running on http://localhost:${PORT}`)
|
||||
})
|
||||
16
backend/src/middleware/auth.ts
Normal file
16
backend/src/middleware/auth.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import fp from 'fastify-plugin'
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
isAuthenticated: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const authMiddleware: FastifyPluginAsync = fp(async (app) => {
|
||||
app.decorateRequest('isAuthenticated', false)
|
||||
app.addHook('onRequest', async (req) => {
|
||||
// hardcoded false — replace with real session/token check when auth is implemented
|
||||
req.isAuthenticated = false
|
||||
})
|
||||
})
|
||||
21
backend/src/middleware/storage.ts
Normal file
21
backend/src/middleware/storage.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import fp from 'fastify-plugin'
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import { SQLiteAdapter } from '../adapters/sqlite.ts'
|
||||
import type { StorageAdapter } from '../types.ts'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
storage: StorageAdapter
|
||||
}
|
||||
}
|
||||
|
||||
const adapter: StorageAdapter = new SQLiteAdapter()
|
||||
|
||||
const plugin: FastifyPluginAsync = async (app) => {
|
||||
app.decorateRequest('storage', null)
|
||||
app.addHook('onRequest', async (req) => {
|
||||
req.storage = adapter
|
||||
})
|
||||
}
|
||||
|
||||
export const storageMiddleware = fp(plugin)
|
||||
10
backend/src/routes/storageMode.ts
Normal file
10
backend/src/routes/storageMode.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
export const storageModeRouter: FastifyPluginAsync = async (app) => {
|
||||
app.get('/', async (req, reply) => {
|
||||
if (!req.isAuthenticated) {
|
||||
return reply.status(401).send({ storageMode: 'local' })
|
||||
}
|
||||
return reply.status(200).send({ storageMode: 'remote' })
|
||||
})
|
||||
}
|
||||
30
backend/src/routes/tickets.ts
Normal file
30
backend/src/routes/tickets.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import type { Ticket } from '../types.ts'
|
||||
|
||||
export const ticketsRouter: FastifyPluginAsync = async (app) => {
|
||||
app.get('/', async (req) => req.storage.getTickets())
|
||||
|
||||
app.get<{ Params: { id: string } }>('/:id', async (req, reply) => {
|
||||
const ticket = req.storage.getTicket(req.params.id)
|
||||
if (!ticket) return reply.status(404).send({ error: 'Not found' })
|
||||
return ticket
|
||||
})
|
||||
|
||||
app.post<{ Body: Pick<Ticket, 'subject' | 'description'> }>('/', async (req, reply) => {
|
||||
const { subject, description } = req.body
|
||||
if (!subject?.trim()) return reply.status(400).send({ error: 'subject is required' })
|
||||
return reply.status(201).send(req.storage.createTicket({ subject, description }))
|
||||
})
|
||||
|
||||
app.patch<{ Params: { id: string }; Body: Partial<Ticket> }>('/:id', async (req, reply) => {
|
||||
const ticket = req.storage.updateTicket(req.params.id, req.body)
|
||||
if (!ticket) return reply.status(404).send({ error: 'Not found' })
|
||||
return ticket
|
||||
})
|
||||
|
||||
app.delete<{ Params: { id: string } }>('/:id', async (req, reply) => {
|
||||
req.storage.deleteTicket(req.params.id)
|
||||
return reply.status(204).send()
|
||||
})
|
||||
}
|
||||
|
||||
24
backend/src/types.ts
Normal file
24
backend/src/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export type TicketType =
|
||||
| "bug"
|
||||
| "billing"
|
||||
| "account"
|
||||
| "feature-request"
|
||||
| "feedback"
|
||||
| "other";
|
||||
|
||||
export interface Ticket {
|
||||
id: string;
|
||||
subject: string;
|
||||
description: string;
|
||||
status: "open" | "in-progress" | "resolved" | "closed";
|
||||
type: TicketType;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface StorageAdapter {
|
||||
getTickets(): Ticket[];
|
||||
getTicket(id: string): Ticket | null;
|
||||
createTicket(data: Pick<Ticket, "subject" | "description" | "type">): Ticket;
|
||||
updateTicket(id: string, patch: Partial<Ticket>): Ticket | null;
|
||||
deleteTicket(id: string): void;
|
||||
}
|
||||
Reference in New Issue
Block a user