elysia-base

This commit is contained in:
Sanjib Kumar Sen 2025-01-07 16:46:50 +06:00
parent af338049c7
commit fe9949c70d
7 changed files with 320 additions and 5 deletions

31
Dockerfile Normal file
View file

@ -0,0 +1,31 @@
# Build stage
FROM oven/bun:1 AS builder
WORKDIR /app
# Copy package files
COPY package.json .
COPY bun.lockb .
# Install dependencies
RUN bun install --frozen-lockfile
# Copy source code
COPY . .
# Build the application
RUN bun build ./src/index.ts --compile --outfile server
# Production stage
FROM debian:bookworm-slim
WORKDIR /app
# Copy only the compiled binary from builder
COPY --from=builder /app/server .
# Expose the port your app runs on
EXPOSE 3000
# Run the binary
CMD ["./server"]

BIN
bun.lockb Executable file

Binary file not shown.

48
docker-compose.yaml Normal file
View file

@ -0,0 +1,48 @@
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- OTEL_EXPORTER_OTLP_ENDPOINT=http://tracing:4318
- OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
networks:
- api-network
depends_on:
- tracing
tracing:
image: jaegertracing/all-in-one:latest
environment:
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
- COLLECTOR_OTLP_ENABLED=true
ports:
# UI
- "16686:16686"
# Zipkin compatible endpoint
- "9411:9411"
# OTLP gRPC
- "4317:4317"
# OTLP HTTP
- "4318:4318"
# Jaeger gRPC
- "14250:14250"
# Jaeger HTTP
- "14268:14268"
# Admin HTTP
- "14269:14269"
# Agent configs
- "5778:5778"
# Thrift compact
- "6831:6831/udp"
# Thrift binary
- "6832:6832/udp"
networks:
- api-network
networks:
api-network:
driver: bridge

View file

@ -3,9 +3,13 @@
"version": "1.0.50", "version": "1.0.50",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun run --watch src/index.ts" "dev": "bun run --watch src/index.ts",
"build": "bun build --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts"
}, },
"dependencies": { "dependencies": {
"@elysiajs/opentelemetry": "^1.2.0",
"@elysiajs/server-timing": "^1.2.0",
"@elysiajs/swagger": "^1.2.0",
"elysia": "latest" "elysia": "latest"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,7 +1,19 @@
import { Elysia } from "elysia"; import { Elysia } from "elysia";
import { swagger } from "@elysiajs/swagger";
import { opentelemetry } from "@elysiajs/opentelemetry";
import { serverTiming } from "@elysiajs/server-timing";
const app = new Elysia().get("/", () => "Hello Elysia").listen(3000); import { note } from "./routes/note";
import { user } from "./routes/user";
console.log( const app = new Elysia()
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` .use(opentelemetry())
); .use(swagger())
.use(serverTiming())
.onError(({ error, code }) => {
if (code === "NOT_FOUND") return "Not Found :(";
console.error(error);
})
.use(user)
.use(note)
.listen(3000);

91
src/routes/note.ts Normal file
View file

@ -0,0 +1,91 @@
import { Elysia, t } from "elysia";
import { getUserId, userService } from "./user";
const memo = t.Object({
data: t.String(),
author: t.String(),
});
type Memo = typeof memo.static;
class Note {
constructor(
public data: Memo[] = [
{
data: "Moonhalo",
author: "saltyaom",
},
]
) {}
add(note: Memo) {
this.data.push(note);
return this.data;
}
remove(index: number) {
return this.data.splice(index, 1);
}
update(index: number, note: Partial<Memo>) {
return (this.data[index] = { ...this.data[index], ...note });
}
}
export const note = new Elysia({ prefix: "/note" })
.use(userService)
.decorate("note", new Note())
.model({
memo: t.Omit(memo, ["author"]),
})
.onTransform(function log({ body, params, path, request: { method } }) {
console.log(`${method} ${path}`, {
body,
params,
});
})
.get("/", ({ note }) => note.data)
.use(getUserId)
.put(
"/",
({ note, body: { data }, username }) =>
note.add({ data, author: username }),
{
body: "memo",
}
)
.get(
"/:index",
({ note, params: { index }, error }) => {
return note.data[index] ?? error(404, "Not Found :(");
},
{
params: t.Object({
index: t.Number(),
}),
}
)
.guard({
params: t.Object({
index: t.Number(),
}),
})
.delete("/:index", ({ note, params: { index }, error }) => {
if (index in note.data) return note.remove(index);
return error(422);
})
.patch(
"/:index",
({ note, params: { index }, body: { data }, error, username }) => {
if (index in note.data)
return note.update(index, { data, author: username });
return error(422);
},
{
isSignIn: true,
body: "memo",
}
);

129
src/routes/user.ts Normal file
View file

@ -0,0 +1,129 @@
import { Elysia, t } from "elysia";
export const userService = new Elysia({ name: "user/service" })
.state({
user: {} as Record<string, string>,
session: {} as Record<number, string>,
})
.model({
signIn: t.Object({
username: t.String({ minLength: 1 }),
password: t.String({ minLength: 8 }),
}),
session: t.Cookie(
{
token: t.Number(),
},
{
secrets: "seia",
}
),
optionalSession: t.Optional(t.Ref("session")),
})
.macro({
isSignIn(enabled: boolean) {
if (!enabled) return;
return {
beforeHandle({ error, cookie: { token }, store: { session } }) {
if (!token.value)
return error(401, {
success: false,
message: "Unauthorized",
});
const username = session[token.value as unknown as number];
if (!username)
return error(401, {
success: false,
message: "Unauthorized",
});
},
};
},
});
export const getUserId = new Elysia()
.use(userService)
.guard({
isSignIn: true,
cookie: "session",
})
.resolve(({ store: { session }, cookie: { token } }) => ({
username: session[token.value],
}))
.as("plugin");
export const user = new Elysia({ prefix: "/user" })
.use(userService)
.put(
"/sign-up",
async ({ body: { username, password }, store, error }) => {
if (store.user[username])
return error(400, {
success: false,
message: "User already exists",
});
store.user[username] = await Bun.password.hash(password);
return {
success: true,
message: "User created",
};
},
{
body: "signIn",
}
)
.post(
"/sign-in",
async ({
store: { user, session },
error,
body: { username, password },
cookie: { token },
}) => {
if (
!user[username] ||
!(await Bun.password.verify(password, user[username]))
)
return error(400, {
success: false,
message: "Invalid username or password",
});
const key = crypto.getRandomValues(new Uint32Array(1))[0];
session[key] = username;
token.value = key;
return {
success: true,
message: `Signed in as ${username}`,
};
},
{
body: "signIn",
cookie: "optionalSession",
}
)
.get(
"/sign-out",
({ cookie: { token } }) => {
token.remove();
return {
success: true,
message: "Signed out",
};
},
{
cookie: "optionalSession",
}
)
.use(getUserId)
.get("/profile", ({ username }) => ({
success: true,
username,
}));