Skip to content

Commit

Permalink
Changed build process and fixed dependency issue
Browse files Browse the repository at this point in the history
  • Loading branch information
maxtheman committed Sep 18, 2024
1 parent 7e715b6 commit 290d98b
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 131 deletions.
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@

# Nuxt.js build / generate output
**/.nuxt
**/dist

# Gatsby files
**/.cache
Expand Down
10 changes: 5 additions & 5 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
DOTENV_PUBLIC_KEY_LOCAL="028c3fff21f9a0a576e7ecb40faf0ec0b25b3c5461a7490eacef8facec30587f0d"

# .env.local
DATABASE_URL="encrypted:BPJWFscpRmV7ulujJkmHLwGKvaMSXoVyW7rsVFtWvQkacfJqYMR27OmMmsGkx0g9C1KKARNBjnjs4PRpVrKJpmd4LTdeXjShtRAScj6BhKLmY1kuQL8V/BINMNr/YnruV+buICSK/vlFRfSTKbQt73/gjkilpQGX2rU="
BASE_URL="encrypted:BGWm0Sjrv6nS8VMcMxnj4B5irVyqY6aj4J4+8gzQP+1nUHpgxRW3vlaf/YZRnnGnN9dJE+fXR/GoX/r08z+QEPffWS3ZK1GMQeAj9dnI3Yie+vfQ7HP47fsn89EgSFSABdpZFQujotdYJjyey2uTjYYBYaFFKw=="
AUTH0_CLIENT_ID="encrypted:BNAL5S3QruIhu50qnbRcagytESh1MZLNeM0lJF6zIjIJzSL4yyzvRxovAGMXjEf07jOtIFNVg8OPquRjXWiYGuikYXN3CNEMxM3ZW6ASX4SNGfR5i5L6sEeqmviF2OU6URokY2pJVwsKXMNFhg7uEZxzQpfgJjp9kWXQAskkmzo2"
AUTH0_DOMAIN="encrypted:BFwm9xX3hqWVE+xcuFW6UTkMlaFbgrU7mRojAco1+fry4L0X+XZTkVV+7LJWUyzLeQ+R8ESerxF8mOK9ILvPMsywtJg9zVAtrxwDHeWfouP3/hrR87kOmZhp4sVXn8FvpMNovGErA3yMzVRuBXh8aJSqLTuyj7+6nbv6eVLYO8EKXCfsJyEWMTCQ"
AUTH0_CLIENT_SECRET="encrypted:BCuSQfxKZl3OapJ9YiK+xT7uZT5o9ihdF/76r3EIYLIBfEMEjyhgl0TvNWiFcu98osV0h9YEMmkWAaiFzO72EJMTXP2xI09MNuKDPLWsnGPXeZhwDUjLAT2+wzjiYumHPL8vwt9cvXWkeLhfv7s5w1920iivELEZyozCPTcMuc4YTlX2Yh6TGfniSF1cFUes8h483N/V00E5TG9gEz08Keg="
DATABASE_URL="encrypted:BP3x+TBLRcl49mS6OBKvudhFGYP0guA+AxuEOnBbHHM0f/HhWTluDD6lTFPKE4gfLPAknVCePhryf2Y11oPFncu2jRAQKhC8PR8zsxWYqebwp/0fSxbAKcVtFsgjQk2oWCVhzsW2+7+c/FJVFv8meRZqmT/IdbHr1Mc="
BASE_URL="encrypted:BJDyPhkTtS1BxicKfNBEc4xsRp1q41KSi44kAU7VsypTT2EZTIHzBOAvfEoN1GnaCfQg0yfbC4WEWP0DKksb2k1eo+Q0li+hE1rtT/aX8WeSx1nIeCTv6KroQZckx0lX+ArjSRvWoMI9j2TI0q6sqt2AWdn/RA=="
AUTH0_CLIENT_ID="encrypted:BCC5mMzUSsMS1hE/StksHKD66HKnTSo+vcnL1YlGLpedb/vWH/TDfmXYmlyswHFr9uF5e/ztSCo4XcFE5l7nW/x5GIZWeIRH+6h5Y5ZO+hxqrZdhWUvT3ZAFsyeoYvWbcCZqcoMMWXJds7S9xhuuXSa6aXQODnAKco9JGESgMz5/"
AUTH0_DOMAIN="encrypted:BJwC0RadJzCnnJDlSDVyBsnsq1bFGc4y1Ir3aWJN4Q3a8c+qmO/AEKXZtzHer09NwWBKL1KWJl37IG3C0TTKp35FAletPppV0YBaGrtwY/qg3plD2w/qhlwxa/I5YG/hwtYlmaM+w+bYGmRNkGzMpPRhIoK613irQq/SSFdF6TRoWgnKZII9D2hE"
AUTH0_CLIENT_SECRET="encrypted:BIlhF009KAcrNcMoVQsUMTbOZOYYJHd6fD3fDTdm72+w1xLEPz0UwFso7/yoeVdIzCgWahJAEnjduRtDj5E1nU0/K+TLcg9hGnB90zM2xH1Np8VRwBPk3z/n+ZNc/mK3MKDtirf6aLIWGBcreYs1uOivJUxp12X71xzTbVNJ3C8c7zKRU/TP1867q7h7uaIz+dvhv45jBCkJXmjGxRYAWlY="
32 changes: 18 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
FROM oven/bun:1
FROM debian:bullseye-slim

RUN apt-get update && apt-get install -y \
sqlite3 \
curl \
&& rm -rf /var/lib/apt/lists/*

RUN apt-get update && apt-get install -y sqlite3 jq
# Download and install dbmate
RUN curl -fsSL -o /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64 && \
chmod +x /usr/local/bin/dbmate

WORKDIR /app

COPY package.json bun.lockb* ./
COPY ./dist/z ./
RUN chmod +x ./z

# Remove or override the prepare script before installing
RUN jq 'del(.scripts.prepare)' package.json > temp.json && mv temp.json package.json

RUN bun install

COPY ./src ./src
RUN mkdir -p ./db
COPY ./db/migrations ./db/migrations
COPY ./db/schema.sql ./db/

COPY src/views ./src/views
COPY src/public ./src/public
COPY src/instrumentation.js ./src/
COPY .env.production ./

EXPOSE 3000

COPY setupDb.sh /setupDb.sh
RUN chmod +x /setupDb.sh
COPY setupDb.sh ./
RUN chmod +x ./setupDb.sh

ENTRYPOINT ["/setupDb.sh"]
CMD ["bun", "start"]
ENTRYPOINT ["./setupDb.sh"]
CMD ["./z"]
Binary file modified bun.lockb
Binary file not shown.
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
"start": "dotenvx run -f .env.production -- bun src/index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"db:migrate": "dotenvx run -f .env.production -- bun run dbmate up",
"prepare": "cp .hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit"
"prepare": "cp .hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit",
"build-linux": "dotenvx run -f .env.production -- bun build --compile --minify --sourcemap --target=bun-linux-x64-modern src/index.js --outfile ./dist/z",
"deploy": "bun run build-linux && fly deploy && bun run clean",
"clean": "rm -rf dist/*"
},
"author": "max caldwell",
"license": "MIT",
Expand All @@ -20,19 +23,18 @@
"@opentelemetry/auto-instrumentations-node": "^0.50.0",
"@opentelemetry/sdk-metrics": "^1.26.0",
"@opentelemetry/sdk-node": "^0.53.0",
"@opentelemetry/sdk-trace-node": "^1.26.0",
"concurrently": "^9.0.1",
"eta": "^3.5.0",
"hono": "^4.6.2",
"install": "^0.13.0",
"jose": "^5.9.2",
"magic-regexp": "^0.8.0",
"pino": "^9.4.0",
"pino-pretty": "^11.2.2"
"pino": "^9.4.0"
},
"devDependencies": {
"dbmate": "^2.20.0",
"eslint": "^9.10.0",
"pino-pretty": "^11.2.2",
"prettier": "^3.3.3",
"prettier-plugin-ejs": "^1.0.3",
"tailwindcss": "^3.4.11"
Expand Down
15 changes: 13 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,16 @@ Mental model for picking this or any stack:
9. Setup CI/CD
- This project uses Github Actions for CD. I don't have experience with other systems.
- Since there is no testing, there's no CI right now, but it's best practice to add that.
10. Go fast.
10. Go fast
- Pick technologies that will run at low-latency and reduced memory requirements.
- Use proper database indexing and best-practices to optimize datareturn speed.
- With this stack and well-optimized queries, the application is responding in single ms.
- This setup optimizes memory and response in part by compiling the JS to a single file and running that in Bun, then because it's all compiled we can use a small docker image like alpine.
11. Handle errors as values
- This is a good idea from Rust and other more functional languages.
- It forces you to handle all possible errors and makes them more obvious.
- In JS, this often means wrapping the function in try/catch and returning the error in a helper as a result type.
- Then, in the imperitive shell of the function, you can just do a simple if (result[1]) { return result[1] } to return the error.


### Things I didn't look into but might be useful:
Expand Down Expand Up @@ -207,7 +214,11 @@ flyctl secrets set DOTENV_PRIVATE_KEY_PRODUCTION='...'
and to deploy changes:

```
fly deploy
bun build-linux && fly deploy
```
or just run
```
bun run deploy
```

## Technologies Used
Expand Down
9 changes: 6 additions & 3 deletions setupDb.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
set -e

# Set the database URL for dbmate
Expand All @@ -18,7 +18,10 @@ if [ ! -f /data/mydb.sqlite ]; then
echo "Database created and migrations applied."
else
echo "Database found, checking for unapplied migrations..."
if dbmate --migrations-dir "$MIGRATIONS_DIR" status | grep -q "Pending"; then
# Extract the number of pending migrations
PENDING=$(dbmate --migrations-dir "$MIGRATIONS_DIR" status | grep "Pending" | awk '{print $2}')

if [ "$PENDING" -gt 0 ]; then
echo "Unapplied migrations found. Applying..."
dbmate --migrations-dir "$MIGRATIONS_DIR" up
echo "Migrations completed."
Expand All @@ -27,5 +30,5 @@ else
fi
fi

# Execute the command passed to the script
echo "Starting the application..."
exec "$@"
143 changes: 53 additions & 90 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,19 @@
import { Hono } from "hono";
import { serveStatic } from "hono/bun";
import path from "path";
import { Eta } from "eta";
import TodoQueries from "./queries";
import { requiresAuth } from "./middleware/auth";
import authRoutes from "./routes/auth";
import { relative, dirname } from 'path';
import { fileURLToPath } from "url";
import { createRegExp, exactly, char, oneOrMore } from 'magic-regexp';
import { logger, opentelemetryMiddleware } from "./instrumentation";


const publicPathRegex = createRegExp(
exactly('/public')
.at.lineStart()
.and(oneOrMore(char))
)
import { findDirectories, handleAsyncError, applyMiddleware } from "./utils";
import { logger } from "./instrumentation";

const app = new Hono();
app.use("*", opentelemetryMiddleware(logger));

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// https://github.com/honojs/hono/issues/2200#issuecomment-2187240226
const relativePathToScript = relative(process.cwd(), __dirname);

const directories = await findDirectories();
applyMiddleware(app, directories);

const views = new Eta({
views: path.join(__dirname, "views"),
views: directories.views,
cache: true,
});

app.use('/public/*', serveStatic({
root: relativePathToScript,
onNotFound: (path, c) => {
console.log(`${path} is not found, you access ${c.req.path}`)
}
}));
app.use("*", async (c, next) => {
if (publicPathRegex.test(c.req.url)) {
return next();
}
const contentType = c.req.header("Content-Type") || "";
if (contentType.includes("application/x-www-form-urlencoded")) {
const body = await c.req.parseBody();
c.req.body = body;
}
await next();
});
// const handleAsyncError = async (fn) => {
// // return [result, error]. If error is not null, it will be an error. If result is not null, it will be the result.
// let result = null;
// let error = null;
// try {
// result = await fn();
// error = null;
// return [result, null];
// } catch (err) {
// logger.error(err);
// error = err;
// return [null, err];
// }
// };

// Mount authentication routes
app.route("/auth", authRoutes);

// Protected Routes
app.get("/", requiresAuth, async (c) => {
logger.info({
user: c.get("user")?.sub,
Expand All @@ -77,68 +23,85 @@ app.get("/", requiresAuth, async (c) => {
});

app.get("/todos", requiresAuth, async (c) => {
try {
const todos = await TodoQueries.getAllTodos(c.get("user")?.sub);
const html = views.render("todos", { todos });
return c.html(html, 200);
} catch (err) {
return c.text("Error: " + err.message, 400);
const [todos, getErr] = await handleAsyncError(() =>
TodoQueries.getAllTodos(c.get("user")?.sub)
);
if (getErr) {
return c.text("Error: " + getErr.message, 400);
}
const html = views.render("todos", { todos });
return c.html(html, 200);
});

app.post("/todos", requiresAuth, async (c) => {
const { listId, title, description, dueDate } = c.req.body;
try {
const result = await TodoQueries.createTodo(
const [result, createErr] = await handleAsyncError(() =>
TodoQueries.createTodo(
listId,
title,
description,
dueDate,
c.get("user")?.sub
);
const newTodo = await TodoQueries.getTodoById(
result.id,
c.get("user")?.sub
);
const html = views.render("todo-item", { todo: newTodo });
return c.html(html, 200);
} catch (err) {
return c.text("Error: " + err.message, 400);
)
);
if (createErr) {
return c.text("Error: " + createErr.message, 400);
}
const [newTodo, getErr] = await handleAsyncError(() =>
TodoQueries.getTodoById(result.id, c.get("user")?.sub)
);
if (getErr) {
return c.text("Error: " + getErr.message, 400);
}
const html = views.render("todo-item", { todo: newTodo });
return c.html(html, 200);
});

app.put("/todos/:id", requiresAuth, async (c) => {
const { id } = c.req.param();
try {
const todo = await TodoQueries.getTodoById(id, c.get("user")?.sub);
const updatedTodo = await TodoQueries.updateTodo(
const [todo, getErr] = await handleAsyncError(() =>
TodoQueries.getTodoById(id, c.get("user")?.sub)
);
if (getErr) {
return c.text("Error: " + getErr.message, 400);
}
const [updatedTodo, updateErr] = await handleAsyncError(() =>
TodoQueries.updateTodo(
id,
todo.title,
todo.description,
!todo.is_completed,
todo.due_date,
c.get("user")?.sub
);
const refreshedTodo = await TodoQueries.getTodoById(id, c.get("user")?.sub);
const html = views.render("todo-item", { todo: refreshedTodo });
return c.html(html, 200);
} catch (err) {
return c.text("Error: " + err.message, 400);
)
);
if (updateErr) {
return c.text("Error: " + updateErr.message, 400);
}
const [refreshedTodo, refreshErr] = await handleAsyncError(() =>
TodoQueries.getTodoById(id, c.get("user")?.sub)
);
if (refreshErr) {
return c.text("Error: " + refreshErr.message, 400);
}
const html = views.render("todo-item", { todo: refreshedTodo });
return c.html(html, 200);
});

app.delete("/todos/:id", requiresAuth, async (c) => {
const { id } = c.req.param();
try {
await TodoQueries.deleteTodo(id, c.get("user")?.sub);
return c.text("", 200);
} catch (err) {
return c.text("Error: " + err.message, 400);
const [_, deleteErr] = await handleAsyncError(() =>
TodoQueries.deleteTodo(id, c.get("user")?.sub)
);
if (deleteErr) {
return c.text("Error: " + deleteErr.message, 400);
}
return c.text("", 200);
});

// Starting the server
app.fire();
logger.info(`Server is running on ${process.env.BASE_URL}`);

export default {
port: 3000,
Expand Down
28 changes: 18 additions & 10 deletions src/instrumentation.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,24 @@ import {
import pino from "pino";
import { context, propagation, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";

// Create a Pino logger
export const logger = pino({
level: "info",
transport: {
target: "pino-pretty",
options: {
colorize: true,
},
},
});
const isProduction = process.env.NODE_ENV === 'production';

export const logger = pino(
isProduction
? { level: 'info' }
: {
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
},
level: 'debug',
}
);


class PinoSpanExporter {
export(spans, resultCallback) {
Expand Down
Loading

0 comments on commit 290d98b

Please sign in to comment.