From dd8e540d7ee07aae9f2c4bb7f377e1e79be41ea0 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 7 Oct 2024 19:00:02 +0700 Subject: [PATCH] fix: run db migration at startup --- .github/workflows/fly-deploy.yml | 18 ++++++++ deno.json | 1 - fly.toml | 26 ++++++----- src/db/db.ts | 76 +++++++++++++++++++------------- src/lnurlp.ts | 73 +++++++++++++++--------------- src/main.ts | 34 ++++++++------ src/nwc/nwcPool.ts | 22 ++++----- src/users.ts | 41 +++++++++-------- 8 files changed, 170 insertions(+), 121 deletions(-) create mode 100644 .github/workflows/fly-deploy.yml diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml new file mode 100644 index 0000000..b0c246e --- /dev/null +++ b/.github/workflows/fly-deploy.yml @@ -0,0 +1,18 @@ +# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ + +name: Fly Deploy +on: + push: + branches: + - main +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + concurrency: deploy-group # optional: ensure only one action runs at a time + steps: + - uses: actions/checkout@v4 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --remote-only + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/deno.json b/deno.json index a33c0c4..bc546d3 100644 --- a/deno.json +++ b/deno.json @@ -9,7 +9,6 @@ "cache": "deno cache ./src/main.ts ./src/db/schema.ts npm:@libsql/client", "cache:reload": "deno cache --reload ./src/main.ts ./src/db/schema.ts", "db:generate": "deno run -A --node-modules-dir npm:drizzle-kit generate", - "db:migrate": "deno run -A --node-modules-dir npm:drizzle-kit migrate", "dev": "deno run --env --allow-net --allow-env --unstable-ffi --allow-ffi --allow-read --allow-write --watch src/main.ts", "start": "deno run --allow-net --allow-env --allow-read=favicon.ico src/main.ts" }, diff --git a/fly.toml b/fly.toml index 65c08ae..b9d1857 100644 --- a/fly.toml +++ b/fly.toml @@ -1,13 +1,16 @@ -app = 'alby-lite' +# fly.toml app configuration file generated for albylite on 2024-09-27T11:16:12+07:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'albylite' primary_region = 'lax' [build] - strategy = "rolling" - max_unavailable = 1 [env] + BASE_URL = 'https://albylite.fly.dev' PORT = '8080' - BASE_URL = "https://albylite.fly.dev" [http_service] internal_port = 8080 @@ -17,13 +20,14 @@ primary_region = 'lax' min_machines_running = 0 processes = ['app'] -[[http_service.checks]] - grace_period = "45s" - interval = "60s" - method = "GET" - timeout = "5s" - path = "/ping" + [[http_service.checks]] + interval = '1m0s' + timeout = '5s' + grace_period = '45s' + method = 'GET' + path = '/ping' + [[vm]] - memory = '1024mb' + memory = '1gb' cpu_kind = 'shared' cpus = 1 diff --git a/src/db/db.ts b/src/db/db.ts index 5b36b01..11432c9 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -1,4 +1,5 @@ -import { drizzle } from "drizzle-orm/postgres-js"; +import { drizzle, PostgresJsDatabase } from "drizzle-orm/postgres-js"; +import { migrate } from "drizzle-orm/postgres-js/migrator"; import { nwc } from "npm:@getalby/sdk"; import postgres from "postgres"; @@ -7,41 +8,54 @@ import { DATABASE_URL } from "../constants.ts"; import * as schema from "./schema.ts"; import { users } from "./schema.ts"; -const queryClient = postgres(DATABASE_URL); -const db = drizzle(queryClient, { - schema, -}); - -export async function createUser( - connectionSecret: string, - username?: string -): Promise<{ username: string }> { - const parsed = nwc.NWCClient.parseWalletConnectUrl(connectionSecret); - if (!parsed.secret) { - throw new Error("no secret found in connection secret"); +export async function runMigration() { + const migrationClient = postgres(DATABASE_URL, { max: 1 }); + await migrate(drizzle(migrationClient), { + migrationsFolder: "./drizzle", + }); +} + +export class DB { + private _db: PostgresJsDatabase; + + constructor() { + const queryClient = postgres(DATABASE_URL); + this._db = drizzle(queryClient, { + schema, + }); } - // TODO: use haikunator - username = username || Math.floor(Math.random() * 100000000000).toString(); + async createUser( + connectionSecret: string, + username?: string + ): Promise<{ username: string }> { + const parsed = nwc.NWCClient.parseWalletConnectUrl(connectionSecret); + if (!parsed.secret) { + throw new Error("no secret found in connection secret"); + } - await db.insert(users).values({ - connectionSecret, - username, - }); + // TODO: use haikunator + username = username || Math.floor(Math.random() * 100000000000).toString(); - return { username }; -} + await this._db.insert(users).values({ + connectionSecret, + username, + }); -export function getAllUsers() { - return db.query.users.findMany(); -} + return { username }; + } -export async function findWalletConnection(username: string) { - const result = await db.query.users.findFirst({ - where: eq(users.username, username), - }); - if (!result) { - throw new Error("user not found"); + getAllUsers() { + return this._db.query.users.findMany(); + } + + async findWalletConnection(username: string) { + const result = await this._db.query.users.findFirst({ + where: eq(users.username, username), + }); + if (!result) { + throw new Error("user not found"); + } + return result?.connectionSecret; } - return result?.connectionSecret; } diff --git a/src/lnurlp.ts b/src/lnurlp.ts index 7c5a037..c5f5bb6 100644 --- a/src/lnurlp.ts +++ b/src/lnurlp.ts @@ -2,53 +2,56 @@ import { Hono } from "hono"; import { nwc } from "npm:@getalby/sdk"; import { logger } from "../src/logger.ts"; import { BASE_URL, DOMAIN } from "./constants.ts"; -import { findWalletConnection } from "./db/db.ts"; +import { DB } from "./db/db.ts"; import "./nwc/nwcPool.ts"; -export const lnurlApp = new Hono(); +export function createLnurlApp(db: DB) { + const hono = new Hono(); -lnurlApp.get("/:username", (c) => { - const username = c.req.param("username"); + hono.get("/:username", (c) => { + const username = c.req.param("username"); - logger.debug("LNURLp request", { username }); + logger.debug("LNURLp request", { username }); - // TODO: zapper support + // TODO: zapper support - return c.json({ - status: "OK", - tag: "payRequest", - commentAllowed: 255, - callback: `${BASE_URL}/.well-known/lnurlp/${username}/callback`, - minSendable: 1000, - maxSendable: 10000000000, - metadata: `[["text/identifier","${username}@${DOMAIN}"],["text/plain","Sats for ${username}"]]`, + return c.json({ + status: "OK", + tag: "payRequest", + commentAllowed: 255, + callback: `${BASE_URL}/.well-known/lnurlp/${username}/callback`, + minSendable: 1000, + maxSendable: 10000000000, + metadata: `[["text/identifier","${username}@${DOMAIN}"],["text/plain","Sats for ${username}"]]`, + }); }); -}); -lnurlApp.get("/:username/callback", async (c) => { - const username = c.req.param("username"); - const amount = c.req.query("amount"); - const comment = c.req.query("comment") || ""; - logger.debug("LNURLp callback", { username, amount, comment }); + hono.get("/:username/callback", async (c) => { + const username = c.req.param("username"); + const amount = c.req.query("amount"); + const comment = c.req.query("comment") || ""; + logger.debug("LNURLp callback", { username, amount, comment }); - // TODO: store data (e.g. for zaps) + // TODO: store data (e.g. for zaps) - if (!amount) { - return c.text('No amount provided', 404); - } + if (!amount) { + return c.text("No amount provided", 404); + } - const connectionSecret = await findWalletConnection(username); + const connectionSecret = await db.findWalletConnection(username); - const nwcClient = new nwc.NWCClient({ - nostrWalletConnectUrl: connectionSecret, - }); + const nwcClient = new nwc.NWCClient({ + nostrWalletConnectUrl: connectionSecret, + }); - const transaction = await nwcClient.makeInvoice({ - amount: +amount, - description: comment, - }); + const transaction = await nwcClient.makeInvoice({ + amount: +amount, + description: comment, + }); - return c.json({ - pr: transaction.invoice, + return c.json({ + pr: transaction.invoice, + }); }); -}); + return hono; +} diff --git a/src/main.ts b/src/main.ts index d66a679..2c3df80 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,35 +1,43 @@ import { Hono } from "hono"; import { serveStatic } from "hono/deno"; import { secureHeaders } from "hono/secure-headers"; -import { loggerMiddleware, LOG_LEVEL, logger } from "./logger.ts"; import { sentry } from "npm:@hono/sentry"; -import { lnurlApp } from "./lnurlp.ts"; import { PORT } from "./constants.ts"; -import { usersApp } from "./users.ts"; +import { DB, runMigration } from "./db/db.ts"; +import { createLnurlApp } from "./lnurlp.ts"; +import { LOG_LEVEL, logger, loggerMiddleware } from "./logger.ts"; +import { NWCPool } from "./nwc/nwcPool.ts"; +import { createUsersApp } from "./users.ts"; + +await runMigration(); + +const db = new DB(); +const nwcPool = new NWCPool(db); +await nwcPool.init(); const SENTRY_DSN = Deno.env.get("SENTRY_DSN"); -const baseApp = new Hono(); +const hono = new Hono(); -baseApp.use(loggerMiddleware()); -baseApp.use(secureHeaders()); +hono.use(loggerMiddleware()); +hono.use(secureHeaders()); if (SENTRY_DSN) { - baseApp.use("*", sentry({ dsn: SENTRY_DSN })); + hono.use("*", sentry({ dsn: SENTRY_DSN })); } -baseApp.route("/.well-known/lnurlp", lnurlApp); -baseApp.route("/users", usersApp); +hono.route("/.well-known/lnurlp", createLnurlApp(db)); +hono.route("/users", createUsersApp(db, nwcPool)); -baseApp.get("/ping", (c) => { +hono.get("/ping", (c) => { return c.body("OK"); }); -baseApp.use("/favicon.ico", serveStatic({ path: "./favicon.ico" })); +hono.use("/favicon.ico", serveStatic({ path: "./favicon.ico" })); -baseApp.get("/robots.txt", (c) => { +hono.get("/robots.txt", (c) => { return c.body("User-agent: *\nDisallow: /", 200); }); -Deno.serve({ port: PORT }, baseApp.fetch); +Deno.serve({ port: PORT }, hono.fetch); logger.info("Server started", { port: PORT, log_level: LOG_LEVEL }); diff --git a/src/nwc/nwcPool.ts b/src/nwc/nwcPool.ts index 2782589..5aad981 100644 --- a/src/nwc/nwcPool.ts +++ b/src/nwc/nwcPool.ts @@ -1,18 +1,20 @@ import { nwc } from "npm:@getalby/sdk"; -import { getAllUsers } from "../db/db.ts"; +import { DB } from "../db/db.ts"; import { logger } from "../logger.ts"; export class NWCPool { - readonly _nwcs: nwc.NWCClient[]; - constructor() { + private readonly _db: DB; + private readonly _nwcs: nwc.NWCClient[]; + constructor(db: DB) { this._nwcs = []; + this._db = db; + } - (async () => { - const users = await getAllUsers(); - for (const user of users) { - this.addNWCClient(user.connectionSecret, user.username); - } - })(); + async init() { + const users = await this._db.getAllUsers(); + for (const user of users) { + this.addNWCClient(user.connectionSecret, user.username); + } } addNWCClient(connectionSecret: string, username: string) { @@ -29,5 +31,3 @@ export class NWCPool { ); } } - -export const nwcPool = new NWCPool(); diff --git a/src/users.ts b/src/users.ts index 3977974..14642ba 100644 --- a/src/users.ts +++ b/src/users.ts @@ -1,31 +1,34 @@ import { Hono } from "hono"; import { DOMAIN } from "./constants.ts"; -import { createUser } from "./db/db.ts"; +import { DB } from "./db/db.ts"; import { logger } from "./logger.ts"; -import { nwcPool } from "./nwc/nwcPool.ts"; +import { NWCPool } from "./nwc/nwcPool.ts"; -export const usersApp = new Hono(); +export function createUsersApp(db: DB, nwcPool: NWCPool) { + const hono = new Hono(); -usersApp.post("/", async (c) => { - logger.debug("create user", {}); + hono.post("/", async (c) => { + logger.debug("create user", {}); - const createUserRequest: { connectionSecret: string; username?: string } = - await c.req.json(); + const createUserRequest: { connectionSecret: string; username?: string } = + await c.req.json(); - if (!createUserRequest.connectionSecret) { - return c.text("no connection secret provided", 400); - } + if (!createUserRequest.connectionSecret) { + return c.text("no connection secret provided", 400); + } - const user = await createUser( - createUserRequest.connectionSecret, - createUserRequest.username - ); + const user = await db.createUser( + createUserRequest.connectionSecret, + createUserRequest.username + ); - const lightningAddress = user.username + "@" + DOMAIN; + const lightningAddress = user.username + "@" + DOMAIN; - nwcPool.addNWCClient(createUserRequest.connectionSecret, user.username); + nwcPool.addNWCClient(createUserRequest.connectionSecret, user.username); - return c.json({ - lightningAddress, + return c.json({ + lightningAddress, + }); }); -}); + return hono; +}