From a4206e1adc53ea9958a43bdae6f32712944844f2 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Sun, 7 Dec 2025 21:18:42 +0000 Subject: [PATCH 01/22] initial bun server --- .gitignore | 13 +++++++++++++ README.md | 18 ++++++++++++++++++ bun.lock | 26 ++++++++++++++++++++++++++ index.ts | 11 +++++++++++ package.json | 16 ++++++++++++++++ tsconfig.json | 29 +++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bun.lock create mode 100644 index.ts create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa523d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules +out +dist +logs + +# environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +.cache \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..02a932e --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# eussi + +The server for Issue Project Manager + +To install dependencies: +```bash +bun install +``` + +To run "dev" script: +```bash +bun dev +``` + +To run "start" script (for deployment): +```bash +bun start +``` \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..478882f --- /dev/null +++ b/bun.lock @@ -0,0 +1,26 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "eussi", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], + + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + + "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..dc06a41 --- /dev/null +++ b/index.ts @@ -0,0 +1,11 @@ +const DEV = process.argv.find(arg => ["--dev", "--developer", "-d"].includes(arg.toLowerCase())) != null +const PORT = process.argv.find(arg => arg.toLowerCase().startsWith("--port="))?.split("=")[1] || "3500" + +const server = Bun.serve({ + port: Number(PORT), + routes: { + "/": () => new Response(`eussi - dev mode: ${DEV}`), + } +}); + +console.log(`eussi (issue server) listening on ${server.url}`); diff --git a/package.json b/package.json new file mode 100644 index 0000000..b823ecb --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "eussi", + "module": "index.ts", + "type": "module", + "private": true, + "scripts": { + "dev": "bun --watch index.ts --dev --PORT=3000", + "start": "bun index.ts --PORT=3000" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} From 7c439710875d70395c27bb07e9bd1473272aa382 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Sun, 7 Dec 2025 22:54:29 +0000 Subject: [PATCH 02/22] biome configuration --- biome.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 biome.json diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..4169a01 --- /dev/null +++ b/biome.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.0.5/schema.json", + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 4, + "lineWidth": 110 + } +} From 87e03b2a62eaefc9fa948673fab4b598bbf71d1a Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Sun, 7 Dec 2025 22:55:02 +0000 Subject: [PATCH 03/22] basic db setup with drizzle --- .env.example | 1 + bun.lock | 224 +++++++++++++++++++++++++++++ docker-compose.yml | 18 +++ drizzle.config.ts | 11 ++ drizzle/0000_chemical_whiplash.sql | 5 + drizzle/0001_short_silver_fox.sql | 1 + drizzle/meta/0000_snapshot.json | 61 ++++++++ drizzle/meta/0001_snapshot.json | 69 +++++++++ drizzle/meta/_journal.json | 20 +++ index.ts | 11 -- package.json | 18 ++- src/db/client.ts | 22 +++ src/db/schema.ts | 7 + src/index.ts | 22 +++ 14 files changed, 475 insertions(+), 15 deletions(-) create mode 100644 .env.example create mode 100644 docker-compose.yml create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_chemical_whiplash.sql create mode 100644 drizzle/0001_short_silver_fox.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 drizzle/meta/_journal.json delete mode 100644 index.ts create mode 100644 src/db/client.ts create mode 100644 src/db/schema.ts create mode 100644 src/index.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5315d10 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +DATABASE_URL=postgres://eussi:password@localhost:5432/issue \ No newline at end of file diff --git a/bun.lock b/bun.lock index 478882f..719e4db 100644 --- a/bun.lock +++ b/bun.lock @@ -4,8 +4,16 @@ "workspaces": { "": { "name": "eussi", + "dependencies": { + "dotenv": "^17.2.3", + "drizzle-orm": "^0.45.0", + "pg": "^8.16.3", + }, "devDependencies": { "@types/bun": "latest", + "@types/pg": "^8.15.6", + "drizzle-kit": "^0.31.8", + "tsx": "^4.21.0", }, "peerDependencies": { "typescript": "^5", @@ -13,14 +21,230 @@ }, }, "packages": { + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + + "drizzle-kit": ["drizzle-kit@0.31.8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="], + + "drizzle-orm": ["drizzle-orm@0.45.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-lyd9VRk3SXKRjV/gQckQzmJgkoYMvVG3A2JAV0vh3L+Lwk+v9+rK5Gj0H22y+ZBmxsrRBgJ5/RbQCN7DWd1dtQ=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], + + "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], + + "pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], + + "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "tsx/esbuild": ["esbuild@0.27.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.1", "@esbuild/android-arm": "0.27.1", "@esbuild/android-arm64": "0.27.1", "@esbuild/android-x64": "0.27.1", "@esbuild/darwin-arm64": "0.27.1", "@esbuild/darwin-x64": "0.27.1", "@esbuild/freebsd-arm64": "0.27.1", "@esbuild/freebsd-x64": "0.27.1", "@esbuild/linux-arm": "0.27.1", "@esbuild/linux-arm64": "0.27.1", "@esbuild/linux-ia32": "0.27.1", "@esbuild/linux-loong64": "0.27.1", "@esbuild/linux-mips64el": "0.27.1", "@esbuild/linux-ppc64": "0.27.1", "@esbuild/linux-riscv64": "0.27.1", "@esbuild/linux-s390x": "0.27.1", "@esbuild/linux-x64": "0.27.1", "@esbuild/netbsd-arm64": "0.27.1", "@esbuild/netbsd-x64": "0.27.1", "@esbuild/openbsd-arm64": "0.27.1", "@esbuild/openbsd-x64": "0.27.1", "@esbuild/openharmony-arm64": "0.27.1", "@esbuild/sunos-x64": "0.27.1", "@esbuild/win32-arm64": "0.27.1", "@esbuild/win32-ia32": "0.27.1", "@esbuild/win32-x64": "0.27.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.1", "", { "os": "android", "cpu": "arm" }, "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.1", "", { "os": "android", "cpu": "arm64" }, "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.1", "", { "os": "android", "cpu": "x64" }, "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.1", "", { "os": "linux", "cpu": "x64" }, "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.1", "", { "os": "none", "cpu": "x64" }, "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="], } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d4686fc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.8' + +services: + db: + image: postgres:16-alpine + container_name: eussi-db + restart: always + environment: + POSTGRES_USER: eussi + POSTGRES_PASSWORD: password + POSTGRES_DB: issue + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..24ff579 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,11 @@ +import "dotenv/config"; +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + out: "./drizzle", + schema: "./src/db/schema.ts", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/drizzle/0000_chemical_whiplash.sql b/drizzle/0000_chemical_whiplash.sql new file mode 100644 index 0000000..57e5796 --- /dev/null +++ b/drizzle/0000_chemical_whiplash.sql @@ -0,0 +1,5 @@ +CREATE TABLE "User" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "User_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "name" varchar(256) NOT NULL, + "username" varchar(32) NOT NULL +); diff --git a/drizzle/0001_short_silver_fox.sql b/drizzle/0001_short_silver_fox.sql new file mode 100644 index 0000000..16bd1ef --- /dev/null +++ b/drizzle/0001_short_silver_fox.sql @@ -0,0 +1 @@ +ALTER TABLE "User" ADD CONSTRAINT "User_username_unique" UNIQUE("username"); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..539ae10 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,61 @@ +{ + "id": "b71bd440-c1af-407a-8499-b543c6b54ee6", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "User_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..00d1872 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,69 @@ +{ + "id": "59167e3d-cddb-4941-aa96-7b4eb1518851", + "prevId": "b71bd440-c1af-407a-8499-b543c6b54ee6", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "User_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "User_username_unique": { + "name": "User_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..4f54ea4 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1765145771181, + "tag": "0000_chemical_whiplash", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1765146859966, + "tag": "0001_short_silver_fox", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/index.ts b/index.ts deleted file mode 100644 index dc06a41..0000000 --- a/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -const DEV = process.argv.find(arg => ["--dev", "--developer", "-d"].includes(arg.toLowerCase())) != null -const PORT = process.argv.find(arg => arg.toLowerCase().startsWith("--port="))?.split("=")[1] || "3500" - -const server = Bun.serve({ - port: Number(PORT), - routes: { - "/": () => new Response(`eussi - dev mode: ${DEV}`), - } -}); - -console.log(`eussi (issue server) listening on ${server.url}`); diff --git a/package.json b/package.json index b823ecb..8ebdff5 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,26 @@ { "name": "eussi", - "module": "index.ts", + "module": "src/index.ts", "type": "module", "private": true, "scripts": { - "dev": "bun --watch index.ts --dev --PORT=3000", - "start": "bun index.ts --PORT=3000" + "dev": "bun --watch src/index.ts --dev --PORT=3000", + "start": "bun src/index.ts --PORT=3000", + "db:start": "docker compose up -d", + "db:stop": "docker compose down" }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "@types/pg": "^8.15.6", + "drizzle-kit": "^0.31.8", + "tsx": "^4.21.0" }, "peerDependencies": { "typescript": "^5" + }, + "dependencies": { + "dotenv": "^17.2.3", + "drizzle-orm": "^0.45.0", + "pg": "^8.16.3" } } diff --git a/src/db/client.ts b/src/db/client.ts new file mode 100644 index 0000000..54265d7 --- /dev/null +++ b/src/db/client.ts @@ -0,0 +1,22 @@ +import "dotenv/config"; +import { drizzle } from "drizzle-orm/node-postgres"; + +if (!process.env.DATABASE_URL) { + throw new Error("DATABASE_URL is not set in environment variables"); +} + +export const db = drizzle({ + connection: { + connectionString: process.env.DATABASE_URL, + }, +}); + +export const testDB = async () => { + try { + await db.execute("SELECT 1;"); + console.log("db connected"); + } catch (err) { + console.log("db down"); + process.exit(); + } +}; diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..05886ac --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,7 @@ +import { integer, pgTable, varchar } from "drizzle-orm/pg-core"; + +export const User = pgTable("User", { + id: integer().primaryKey().generatedAlwaysAsIdentity(), + name: varchar({ length: 256 }).notNull(), + username: varchar({ length: 32 }).notNull().unique(), +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..21e173e --- /dev/null +++ b/src/index.ts @@ -0,0 +1,22 @@ +import { db, testDB } from "./db/client"; +import { User } from "./db/schema"; + +const DEV = process.argv.find((arg) => ["--dev", "--developer", "-d"].includes(arg.toLowerCase())) != null; +const PORT = process.argv.find((arg) => arg.toLowerCase().startsWith("--port="))?.split("=")[1] || "3500"; + +const main = async () => { + const server = Bun.serve({ + port: Number(PORT), + routes: { + "/": () => new Response(`title: eussi\ndev-mode: ${DEV}\nport: ${PORT}`), + }, + }); + + console.log(`eussi (issue server) listening on ${server.url}`); + await testDB(); + + const users = await db.select().from(User); + console.log(`serving ${users.length} user${users.length === 1 ? "" : "s"}`); +}; + +main(); From 100b916d5dc580f15e75a540795fb33bdc9e3e8c Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:03:12 +0000 Subject: [PATCH 04/22] Project table --- src/db/schema.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/db/schema.ts b/src/db/schema.ts index 05886ac..8e2a6f8 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -5,3 +5,12 @@ export const User = pgTable("User", { name: varchar({ length: 256 }).notNull(), username: varchar({ length: 32 }).notNull().unique(), }); + +export const Project = pgTable("Project", { + id: integer().primaryKey().generatedAlwaysAsIdentity(), + blob: varchar({ length: 4 }).notNull(), + name: varchar({ length: 256 }).notNull(), + ownerId: integer() + .notNull() + .references(() => User.id), +}); From b324ab3e2f0f9f07eda061ad409132661633b87d Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:04:42 +0000 Subject: [PATCH 05/22] Issue table --- src/db/schema.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/db/schema.ts b/src/db/schema.ts index 8e2a6f8..4b0ec4f 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,4 +1,4 @@ -import { integer, pgTable, varchar } from "drizzle-orm/pg-core"; +import { integer, pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"; export const User = pgTable("User", { id: integer().primaryKey().generatedAlwaysAsIdentity(), @@ -14,3 +14,23 @@ export const Project = pgTable("Project", { .notNull() .references(() => User.id), }); + +export const Issue = pgTable( + "Issue", + { + id: integer().primaryKey().generatedAlwaysAsIdentity(), + projectId: integer() + .notNull() + .references(() => Project.id), + + number: integer("number").notNull(), + + title: varchar({ length: 256 }).notNull(), + description: varchar({ length: 2048 }).notNull(), + }, + (t) => [ + // ensures unique numbers per project + // you can have Issue 1 in PROJ and Issue 1 in TEST, but not two Issue 1s in PROJ + uniqueIndex("unique_project_issue_number").on(t.projectId, t.number), + ], +); From 3fecdd15e37be78d73dec2074bba6c99b771d3e1 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:05:17 +0000 Subject: [PATCH 06/22] Project & Issue migration --- drizzle/0002_abnormal_fenris.sql | 18 +++ drizzle/meta/0002_snapshot.json | 221 +++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + 3 files changed, 246 insertions(+) create mode 100644 drizzle/0002_abnormal_fenris.sql create mode 100644 drizzle/meta/0002_snapshot.json diff --git a/drizzle/0002_abnormal_fenris.sql b/drizzle/0002_abnormal_fenris.sql new file mode 100644 index 0000000..862c20f --- /dev/null +++ b/drizzle/0002_abnormal_fenris.sql @@ -0,0 +1,18 @@ +CREATE TABLE "Issue" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "Issue_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "projectId" integer NOT NULL, + "number" integer NOT NULL, + "title" varchar(256) NOT NULL, + "description" varchar(2048) NOT NULL +); +--> statement-breakpoint +CREATE TABLE "Project" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "Project_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "blob" varchar(4) NOT NULL, + "name" varchar(256) NOT NULL, + "ownerId" integer NOT NULL +); +--> statement-breakpoint +ALTER TABLE "Issue" ADD CONSTRAINT "Issue_projectId_Project_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."Project"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "Project" ADD CONSTRAINT "Project_ownerId_User_id_fk" FOREIGN KEY ("ownerId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "unique_project_issue_number" ON "Issue" USING btree ("projectId","number"); \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..6b84322 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,221 @@ +{ + "id": "b57ed503-91cd-439b-888a-a7181acd1819", + "prevId": "59167e3d-cddb-4941-aa96-7b4eb1518851", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Issue": { + "name": "Issue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Issue_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "unique_project_issue_number": { + "name": "unique_project_issue_number", + "columns": [ + { + "expression": "projectId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Issue_projectId_Project_id_fk": { + "name": "Issue_projectId_Project_id_fk", + "tableFrom": "Issue", + "tableTo": "Project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Project": { + "name": "Project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Project_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "blob": { + "name": "blob", + "type": "varchar(4)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "ownerId": { + "name": "ownerId", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Project_ownerId_User_id_fk": { + "name": "Project_ownerId_User_id_fk", + "tableFrom": "Project", + "tableTo": "User", + "columnsFrom": [ + "ownerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "User_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "User_username_unique": { + "name": "User_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 4f54ea4..10513b9 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1765146859966, "tag": "0001_short_silver_fox", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1765149910114, + "tag": "0002_abnormal_fenris", + "breakpoints": true } ] } \ No newline at end of file From d1996e7fe7b01ae531d3d4d4d63205266aa61ee7 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:06:43 +0000 Subject: [PATCH 07/22] pre-made queries for users, projects, and issues --- src/db/queries.ts | 92 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/db/queries.ts diff --git a/src/db/queries.ts b/src/db/queries.ts new file mode 100644 index 0000000..8b0c517 --- /dev/null +++ b/src/db/queries.ts @@ -0,0 +1,92 @@ +import { eq, sql, and } from "drizzle-orm"; +import { db } from "./client"; +import { Issue, Project, User } from "./schema"; + +// user related +export async function createUser(name: string, username: string) { + const [user] = await db.insert(User).values({ name, username }).returning(); + return user; +} + +export async function getUserByUsername(username: string) { + const [user] = await db.select().from(User).where(eq(User.username, username)); + return user; +} + +// project related + +export async function createProject(blob: string, name: string, owner: typeof User.$inferSelect) { + const [project] = await db + .insert(Project) + .values({ + blob, + name, + ownerId: owner.id, + }) + .returning(); + if (!project) { + throw new Error(`failed to create project ${name} with blob ${blob} for owner ${owner.username}`); + } + return project; +} + +export async function getProjectByBlob(projectBlob: string) { + const [issue] = await db.select().from(Project).where(eq(Project.blob, projectBlob)); + return issue; +} + +// issue related +export async function createIssue(projectId: number, title: string, description: string) { + // prevents two issues with the same unique number + return await db.transaction(async (tx) => { + // raw sql for speed + // most recent issue from project + const [lastIssue] = await tx + .select({ max: sql`MAX(${Issue.number})` }) + .from(Issue) + .where(eq(Issue.projectId, projectId)); + + const nextNumber = (lastIssue?.max || 0) + 1; + + // 2. create new issue + const [newIssue] = await tx + .insert(Issue) + .values({ + projectId, + title, + description, + number: nextNumber, + }) + .returning(); + + return newIssue; + }); +} + +export async function deleteIssue(projectId: number, number: number) { + return await db.delete(Issue).where(and(eq(Issue.projectId, projectId), eq(Issue.number, number))); +} + +export async function updateIssue( + projectId: number, + number: number, + updates: { title?: string; description?: string }, +) { + return await db + .update(Issue) + .set(updates) + .where(and(eq(Issue.projectId, projectId), eq(Issue.number, number))) + .returning(); +} + +export async function getIssuesByProject(projectId: number) { + return await db.select().from(Issue).where(eq(Issue.projectId, projectId)); +} + +export async function getIssueByNumber(projectId: number, number: number) { + const [issue] = await db + .select() + .from(Issue) + .where(and(eq(Issue.projectId, projectId), eq(Issue.number, number))); + return issue; +} From 31f96ff846b951fd074217f1e59da17eeb23836d Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:08:19 +0000 Subject: [PATCH 08/22] demo data --- src/index.ts | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 21e173e..8f444c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,29 @@ import { db, testDB } from "./db/client"; import { User } from "./db/schema"; +import { createUser, createIssue, createProject } from "./db/queries"; const DEV = process.argv.find((arg) => ["--dev", "--developer", "-d"].includes(arg.toLowerCase())) != null; -const PORT = process.argv.find((arg) => arg.toLowerCase().startsWith("--port="))?.split("=")[1] || "3500"; +const PORT = process.argv.find((arg) => arg.toLowerCase().startsWith("--port="))?.split("=")[1] || 0; + +const createDemoData = async () => { + const user = await createUser("Demo User", "demo_user"); + if (!user) { + throw new Error("failed to create demo user"); + } + + const projectNames = ["PROJ", "TEST", "SAMPLE"]; + for (const name of projectNames) { + const project = await createProject(name.slice(0, 4), name, user); + + for (let i = 1; i <= 5; i++) { + await createIssue( + project.id, + `Issue ${i} in ${name}`, + `This is a description for issue ${i} in ${name}.`, + ); + } + } +}; const main = async () => { const server = Bun.serve({ @@ -15,7 +36,15 @@ const main = async () => { console.log(`eussi (issue server) listening on ${server.url}`); await testDB(); - const users = await db.select().from(User); + let users = await db.select().from(User); + + if (DEV && users.length === 0) { + console.log("creating demo data..."); + await createDemoData(); + console.log("demo data created"); + users = await db.select().from(User); + } + console.log(`serving ${users.length} user${users.length === 1 ? "" : "s"}`); }; From f484b99067fdfa86ddf401f905ce2b5afa20916d Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:08:34 +0000 Subject: [PATCH 09/22] "issues/:projectId" route --- src/index.ts | 2 ++ src/routes/index.ts | 5 +++++ src/routes/issues.ts | 14 ++++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 src/routes/index.ts create mode 100644 src/routes/issues.ts diff --git a/src/index.ts b/src/index.ts index 8f444c8..e50c5a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { db, testDB } from "./db/client"; import { User } from "./db/schema"; +import { routes } from "./routes"; import { createUser, createIssue, createProject } from "./db/queries"; const DEV = process.argv.find((arg) => ["--dev", "--developer", "-d"].includes(arg.toLowerCase())) != null; @@ -30,6 +31,7 @@ const main = async () => { port: Number(PORT), routes: { "/": () => new Response(`title: eussi\ndev-mode: ${DEV}\nport: ${PORT}`), + "/issues/:projectId": routes.issues, }, }); diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..e47145c --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,5 @@ +import issues from "./issues"; + +export const routes = { + issues, +}; diff --git a/src/routes/issues.ts b/src/routes/issues.ts new file mode 100644 index 0000000..52b247a --- /dev/null +++ b/src/routes/issues.ts @@ -0,0 +1,14 @@ +import type { BunRequest } from "bun"; +import { getIssuesByProject, getProjectByBlob } from "../db/queries.js"; + +export default async function issues(req: BunRequest<"/issues/:projectId">) { + const { projectId } = req.params; + + const project = await getProjectByBlob(projectId); + if (!project) { + return new Response("project not found", { status: 404 }); + } + const issues = await getIssuesByProject(project.id); + + return Response.json(issues); +} From db69501a2f903606281436741e47744b7d7db18a Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:08:41 +0000 Subject: [PATCH 10/22] db migration scripts --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8ebdff5..5bdb664 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "dev": "bun --watch src/index.ts --dev --PORT=3000", "start": "bun src/index.ts --PORT=3000", "db:start": "docker compose up -d", - "db:stop": "docker compose down" + "db:stop": "docker compose down", + "db:migrate": "npx drizzle-kit generate && npx drizzle-kit migrate", + "db:push": "npx drizzle-kit push" }, "devDependencies": { "@types/bun": "latest", From 8e76b0ce3837c322d6727cd4689d2895c3796fa8 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:16:20 +0000 Subject: [PATCH 11/22] moved demo data creation to util file --- src/index.ts | 22 +--------------------- src/utils.ts | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 src/utils.ts diff --git a/src/index.ts b/src/index.ts index e50c5a6..f7a0fb6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,31 +1,11 @@ import { db, testDB } from "./db/client"; import { User } from "./db/schema"; import { routes } from "./routes"; -import { createUser, createIssue, createProject } from "./db/queries"; +import { createDemoData } from "./utils"; const DEV = process.argv.find((arg) => ["--dev", "--developer", "-d"].includes(arg.toLowerCase())) != null; const PORT = process.argv.find((arg) => arg.toLowerCase().startsWith("--port="))?.split("=")[1] || 0; -const createDemoData = async () => { - const user = await createUser("Demo User", "demo_user"); - if (!user) { - throw new Error("failed to create demo user"); - } - - const projectNames = ["PROJ", "TEST", "SAMPLE"]; - for (const name of projectNames) { - const project = await createProject(name.slice(0, 4), name, user); - - for (let i = 1; i <= 5; i++) { - await createIssue( - project.id, - `Issue ${i} in ${name}`, - `This is a description for issue ${i} in ${name}.`, - ); - } - } -}; - const main = async () => { const server = Bun.serve({ port: Number(PORT), diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..d80b3de --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,21 @@ +import { createIssue, createProject, createUser } from "./db/queries"; + +export const createDemoData = async () => { + const user = await createUser("Demo User", "demo_user"); + if (!user) { + throw new Error("failed to create demo user"); + } + + const projectNames = ["PROJ", "TEST", "SAMPLE"]; + for (const name of projectNames) { + const project = await createProject(name.slice(0, 4), name, user); + + for (let i = 1; i <= 5; i++) { + await createIssue( + project.id, + `Issue ${i} in ${name}`, + `This is a description for issue ${i} in ${name}.`, + ); + } + } +}; From b45debd8ad78b61822d94be241f8b6427930cd00 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:29:57 +0000 Subject: [PATCH 12/22] getProjectByID --- src/db/queries.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/db/queries.ts b/src/db/queries.ts index 8b0c517..43f836c 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -30,9 +30,14 @@ export async function createProject(blob: string, name: string, owner: typeof Us return project; } +export async function getProjectByID(projectId: number) { + const [project] = await db.select().from(Project).where(eq(Project.id, projectId)); + return project; +} + export async function getProjectByBlob(projectBlob: string) { - const [issue] = await db.select().from(Project).where(eq(Project.blob, projectBlob)); - return issue; + const [project] = await db.select().from(Project).where(eq(Project.blob, projectBlob)); + return project; } // issue related From 59c96c64fc6e9e85b81aea32a4cf5f2fd8a80ab3 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:30:23 +0000 Subject: [PATCH 13/22] /issue/create --- src/index.ts | 1 + src/routes/index.ts | 2 ++ src/routes/issueCreate.ts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 src/routes/issueCreate.ts diff --git a/src/index.ts b/src/index.ts index f7a0fb6..266e9db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ const main = async () => { port: Number(PORT), routes: { "/": () => new Response(`title: eussi\ndev-mode: ${DEV}\nport: ${PORT}`), + "/issue/create": routes.issueCreate, "/issues/:projectId": routes.issues, }, }); diff --git a/src/routes/index.ts b/src/routes/index.ts index e47145c..3a595e2 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,5 +1,7 @@ +import issueCreate from "./issueCreate" import issues from "./issues"; export const routes = { + issueCreate, issues, }; diff --git a/src/routes/issueCreate.ts b/src/routes/issueCreate.ts new file mode 100644 index 0000000..9596b73 --- /dev/null +++ b/src/routes/issueCreate.ts @@ -0,0 +1,31 @@ +import type { BunRequest } from "bun"; +import { createIssue, getProjectByID, getProjectByBlob } from "../db/queries.js"; + +// /issue/create?projectId=1&title=Testing&description=Description +// OR +// /issue/create?projectBlob=projectBlob&title=Testing&description=Description +export default async function issueCreate(req: BunRequest) { + const url = new URL(req.url); + const projectId = url.searchParams.get("projectId"); + const projectBlob = url.searchParams.get("projectBlob"); + + let project = null; + if (projectId) { + project = await getProjectByID(Number(projectId)); + } else if (projectBlob) { + project = await getProjectByBlob(projectBlob); + } else { + return new Response("missing project blob or project id", { status: 400 }); + } + if (!project) { + return new Response(`project not found: provided ${projectId ?? projectBlob}`, { status: 404 }); + } + + const title = url.searchParams.get("title") || "Untitled Issue"; + const description = url.searchParams.get("description") || ""; + + const issue = await createIssue(project.id, title, description); + + return Response.json(issue); +} + \ No newline at end of file From c30fa97ce702275d4e9d611b790b86a254701832 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:34:52 +0000 Subject: [PATCH 14/22] issueUpdate --- src/db/queries.ts | 12 ++---------- src/index.ts | 1 + src/routes/index.ts | 4 +++- src/routes/issueUpdate.ts | 24 ++++++++++++++++++++++++ 4 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 src/routes/issueUpdate.ts diff --git a/src/db/queries.ts b/src/db/queries.ts index 43f836c..738d4c1 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -72,16 +72,8 @@ export async function deleteIssue(projectId: number, number: number) { return await db.delete(Issue).where(and(eq(Issue.projectId, projectId), eq(Issue.number, number))); } -export async function updateIssue( - projectId: number, - number: number, - updates: { title?: string; description?: string }, -) { - return await db - .update(Issue) - .set(updates) - .where(and(eq(Issue.projectId, projectId), eq(Issue.number, number))) - .returning(); +export async function updateIssue(id: number, updates: { title?: string; description?: string }) { + return await db.update(Issue).set(updates).where(eq(Issue.id, id)).returning(); } export async function getIssuesByProject(projectId: number) { diff --git a/src/index.ts b/src/index.ts index 266e9db..b8dd982 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ const main = async () => { routes: { "/": () => new Response(`title: eussi\ndev-mode: ${DEV}\nport: ${PORT}`), "/issue/create": routes.issueCreate, + "/issue/update": routes.issueUpdate, "/issues/:projectId": routes.issues, }, }); diff --git a/src/routes/index.ts b/src/routes/index.ts index 3a595e2..4d7ab14 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,7 +1,9 @@ -import issueCreate from "./issueCreate" +import issueCreate from "./issueCreate"; import issues from "./issues"; +import issueUpdate from "./issueUpdate"; export const routes = { issueCreate, issues, + issueUpdate, }; diff --git a/src/routes/issueUpdate.ts b/src/routes/issueUpdate.ts new file mode 100644 index 0000000..34cccca --- /dev/null +++ b/src/routes/issueUpdate.ts @@ -0,0 +1,24 @@ +import type { BunRequest } from "bun"; +import { updateIssue } from "../db/queries.js"; + +// /issue/update?id=1&title=Testing&description=Description +export default async function issueUpdate(req: BunRequest) { + const url = new URL(req.url); + const id = url.searchParams.get("id"); + if (!id) { + return new Response("missing issue id", { status: 400 }); + } + + const title = url.searchParams.get("title") || undefined; + const description = url.searchParams.get("description") || undefined; + if (!title && !description) { + return new Response("no updates provided", { status: 400 }); + } + + const issue = await updateIssue(Number(id), { + title, + description, + }); + + return Response.json(issue); +} From c256afdbefb08682f94b92cf09a3812f6c6a158e Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:34:52 +0000 Subject: [PATCH 15/22] /issue/update --- src/db/queries.ts | 12 ++---------- src/index.ts | 1 + src/routes/index.ts | 4 +++- src/routes/issueUpdate.ts | 24 ++++++++++++++++++++++++ 4 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 src/routes/issueUpdate.ts diff --git a/src/db/queries.ts b/src/db/queries.ts index 43f836c..738d4c1 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -72,16 +72,8 @@ export async function deleteIssue(projectId: number, number: number) { return await db.delete(Issue).where(and(eq(Issue.projectId, projectId), eq(Issue.number, number))); } -export async function updateIssue( - projectId: number, - number: number, - updates: { title?: string; description?: string }, -) { - return await db - .update(Issue) - .set(updates) - .where(and(eq(Issue.projectId, projectId), eq(Issue.number, number))) - .returning(); +export async function updateIssue(id: number, updates: { title?: string; description?: string }) { + return await db.update(Issue).set(updates).where(eq(Issue.id, id)).returning(); } export async function getIssuesByProject(projectId: number) { diff --git a/src/index.ts b/src/index.ts index 266e9db..b8dd982 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ const main = async () => { routes: { "/": () => new Response(`title: eussi\ndev-mode: ${DEV}\nport: ${PORT}`), "/issue/create": routes.issueCreate, + "/issue/update": routes.issueUpdate, "/issues/:projectId": routes.issues, }, }); diff --git a/src/routes/index.ts b/src/routes/index.ts index 3a595e2..4d7ab14 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,7 +1,9 @@ -import issueCreate from "./issueCreate" +import issueCreate from "./issueCreate"; import issues from "./issues"; +import issueUpdate from "./issueUpdate"; export const routes = { issueCreate, issues, + issueUpdate, }; diff --git a/src/routes/issueUpdate.ts b/src/routes/issueUpdate.ts new file mode 100644 index 0000000..34cccca --- /dev/null +++ b/src/routes/issueUpdate.ts @@ -0,0 +1,24 @@ +import type { BunRequest } from "bun"; +import { updateIssue } from "../db/queries.js"; + +// /issue/update?id=1&title=Testing&description=Description +export default async function issueUpdate(req: BunRequest) { + const url = new URL(req.url); + const id = url.searchParams.get("id"); + if (!id) { + return new Response("missing issue id", { status: 400 }); + } + + const title = url.searchParams.get("title") || undefined; + const description = url.searchParams.get("description") || undefined; + if (!title && !description) { + return new Response("no updates provided", { status: 400 }); + } + + const issue = await updateIssue(Number(id), { + title, + description, + }); + + return Response.json(issue); +} From 215cab0f6257f9ee297390194312874894ed5b4b Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:42:36 +0000 Subject: [PATCH 16/22] deleteIssue now uses issue's id instead of projectId + number --- src/db/queries.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/db/queries.ts b/src/db/queries.ts index 738d4c1..8535542 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -68,8 +68,8 @@ export async function createIssue(projectId: number, title: string, description: }); } -export async function deleteIssue(projectId: number, number: number) { - return await db.delete(Issue).where(and(eq(Issue.projectId, projectId), eq(Issue.number, number))); +export async function deleteIssue(id: number) { + return await db.delete(Issue).where(eq(Issue.id, id)); } export async function updateIssue(id: number, updates: { title?: string; description?: string }) { From 4b428782e239fd8505ad3b4dd73813f698da7ab4 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:42:41 +0000 Subject: [PATCH 17/22] /issue/delete --- src/index.ts | 1 + src/routes/index.ts | 2 ++ src/routes/issueDelete.ts | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 src/routes/issueDelete.ts diff --git a/src/index.ts b/src/index.ts index b8dd982..c3e1b94 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ const main = async () => { "/": () => new Response(`title: eussi\ndev-mode: ${DEV}\nport: ${PORT}`), "/issue/create": routes.issueCreate, "/issue/update": routes.issueUpdate, + "/issue/delete": routes.issueDelete, "/issues/:projectId": routes.issues, }, }); diff --git a/src/routes/index.ts b/src/routes/index.ts index 4d7ab14..8513cde 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,9 +1,11 @@ import issueCreate from "./issueCreate"; +import issueDelete from "./issueDelete"; import issues from "./issues"; import issueUpdate from "./issueUpdate"; export const routes = { issueCreate, + issueDelete, issues, issueUpdate, }; diff --git a/src/routes/issueDelete.ts b/src/routes/issueDelete.ts new file mode 100644 index 0000000..e1cc6bc --- /dev/null +++ b/src/routes/issueDelete.ts @@ -0,0 +1,18 @@ +import type { BunRequest } from "bun"; +import { deleteIssue } from "../db/queries.js"; + +// /issue/delete?id=1 +export default async function issueDelete(req: BunRequest) { + const url = new URL(req.url); + const id = url.searchParams.get("id"); + if (!id) { + return new Response("missing issue id", { status: 400 }); + } + + const result = await deleteIssue(Number(id)); + if (result.rowCount === 0) { + return new Response(`no issue with id ${id} found`, { status: 404 }); + } + + return new Response(`issue with id ${id} deleted`, { status: 200 }); +} From 01e32a8177b8499d23aa2c1a8ae95c358991b626 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:52:08 +0000 Subject: [PATCH 18/22] new queries project structure --- src/db/queries/index.ts | 3 ++ src/db/{queries.ts => queries/issues.ts} | 43 ++---------------------- src/db/queries/projects.ts | 28 +++++++++++++++ src/db/queries/users.ts | 13 +++++++ src/routes/issueCreate.ts | 2 +- src/routes/issueDelete.ts | 2 +- src/routes/issueUpdate.ts | 2 +- src/routes/issues.ts | 2 +- 8 files changed, 50 insertions(+), 45 deletions(-) create mode 100644 src/db/queries/index.ts rename src/db/{queries.ts => queries/issues.ts} (55%) create mode 100644 src/db/queries/projects.ts create mode 100644 src/db/queries/users.ts diff --git a/src/db/queries/index.ts b/src/db/queries/index.ts new file mode 100644 index 0000000..4fc4e73 --- /dev/null +++ b/src/db/queries/index.ts @@ -0,0 +1,3 @@ +export * from "./users"; +export * from "./projects"; +export * from "./issues"; diff --git a/src/db/queries.ts b/src/db/queries/issues.ts similarity index 55% rename from src/db/queries.ts rename to src/db/queries/issues.ts index 8535542..adc7c20 100644 --- a/src/db/queries.ts +++ b/src/db/queries/issues.ts @@ -1,46 +1,7 @@ import { eq, sql, and } from "drizzle-orm"; -import { db } from "./client"; -import { Issue, Project, User } from "./schema"; +import { db } from "../client"; +import { Issue } from "../schema"; -// user related -export async function createUser(name: string, username: string) { - const [user] = await db.insert(User).values({ name, username }).returning(); - return user; -} - -export async function getUserByUsername(username: string) { - const [user] = await db.select().from(User).where(eq(User.username, username)); - return user; -} - -// project related - -export async function createProject(blob: string, name: string, owner: typeof User.$inferSelect) { - const [project] = await db - .insert(Project) - .values({ - blob, - name, - ownerId: owner.id, - }) - .returning(); - if (!project) { - throw new Error(`failed to create project ${name} with blob ${blob} for owner ${owner.username}`); - } - return project; -} - -export async function getProjectByID(projectId: number) { - const [project] = await db.select().from(Project).where(eq(Project.id, projectId)); - return project; -} - -export async function getProjectByBlob(projectBlob: string) { - const [project] = await db.select().from(Project).where(eq(Project.blob, projectBlob)); - return project; -} - -// issue related export async function createIssue(projectId: number, title: string, description: string) { // prevents two issues with the same unique number return await db.transaction(async (tx) => { diff --git a/src/db/queries/projects.ts b/src/db/queries/projects.ts new file mode 100644 index 0000000..b81e2de --- /dev/null +++ b/src/db/queries/projects.ts @@ -0,0 +1,28 @@ +import { eq } from "drizzle-orm"; +import { db } from "../client"; +import { Project, User } from "../schema"; + +export async function createProject(blob: string, name: string, owner: typeof User.$inferSelect) { + const [project] = await db + .insert(Project) + .values({ + blob, + name, + ownerId: owner.id, + }) + .returning(); + if (!project) { + throw new Error(`failed to create project ${name} with blob ${blob} for owner ${owner.username}`); + } + return project; +} + +export async function getProjectByID(projectId: number) { + const [project] = await db.select().from(Project).where(eq(Project.id, projectId)); + return project; +} + +export async function getProjectByBlob(projectBlob: string) { + const [project] = await db.select().from(Project).where(eq(Project.blob, projectBlob)); + return project; +} diff --git a/src/db/queries/users.ts b/src/db/queries/users.ts new file mode 100644 index 0000000..5372ff8 --- /dev/null +++ b/src/db/queries/users.ts @@ -0,0 +1,13 @@ +import { eq } from "drizzle-orm"; +import { db } from "../client"; +import { User } from "../schema"; + +export async function createUser(name: string, username: string) { + const [user] = await db.insert(User).values({ name, username }).returning(); + return user; +} + +export async function getUserByUsername(username: string) { + const [user] = await db.select().from(User).where(eq(User.username, username)); + return user; +} diff --git a/src/routes/issueCreate.ts b/src/routes/issueCreate.ts index 9596b73..260cbb7 100644 --- a/src/routes/issueCreate.ts +++ b/src/routes/issueCreate.ts @@ -1,5 +1,5 @@ import type { BunRequest } from "bun"; -import { createIssue, getProjectByID, getProjectByBlob } from "../db/queries.js"; +import { createIssue, getProjectByID, getProjectByBlob } from "../db/queries"; // /issue/create?projectId=1&title=Testing&description=Description // OR diff --git a/src/routes/issueDelete.ts b/src/routes/issueDelete.ts index e1cc6bc..14ab0a5 100644 --- a/src/routes/issueDelete.ts +++ b/src/routes/issueDelete.ts @@ -1,5 +1,5 @@ import type { BunRequest } from "bun"; -import { deleteIssue } from "../db/queries.js"; +import { deleteIssue } from "../db/queries"; // /issue/delete?id=1 export default async function issueDelete(req: BunRequest) { diff --git a/src/routes/issueUpdate.ts b/src/routes/issueUpdate.ts index 34cccca..a428a22 100644 --- a/src/routes/issueUpdate.ts +++ b/src/routes/issueUpdate.ts @@ -1,5 +1,5 @@ import type { BunRequest } from "bun"; -import { updateIssue } from "../db/queries.js"; +import { updateIssue } from "../db/queries"; // /issue/update?id=1&title=Testing&description=Description export default async function issueUpdate(req: BunRequest) { diff --git a/src/routes/issues.ts b/src/routes/issues.ts index 52b247a..c70cdf9 100644 --- a/src/routes/issues.ts +++ b/src/routes/issues.ts @@ -1,5 +1,5 @@ import type { BunRequest } from "bun"; -import { getIssuesByProject, getProjectByBlob } from "../db/queries.js"; +import { getIssuesByProject, getProjectByBlob } from "../db/queries"; export default async function issues(req: BunRequest<"/issues/:projectId">) { const { projectId } = req.params; From b631fbaada620ff6c7151455d15106b29d9be80f Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 00:59:52 +0000 Subject: [PATCH 19/22] better route project structure --- src/db/queries/issues.ts | 9 +++++++++ src/index.ts | 2 +- src/routes/index.ts | 10 +++++----- src/routes/{issueCreate.ts => issue/create.ts} | 3 +-- src/routes/{issueDelete.ts => issue/delete.ts} | 2 +- src/routes/{issueUpdate.ts => issue/update.ts} | 2 +- src/routes/issues.ts | 14 -------------- src/routes/issues/[projectBlob].ts | 14 ++++++++++++++ 8 files changed, 32 insertions(+), 24 deletions(-) rename src/routes/{issueCreate.ts => issue/create.ts} (98%) rename src/routes/{issueDelete.ts => issue/delete.ts} (91%) rename src/routes/{issueUpdate.ts => issue/update.ts} (93%) delete mode 100644 src/routes/issues.ts create mode 100644 src/routes/issues/[projectBlob].ts diff --git a/src/db/queries/issues.ts b/src/db/queries/issues.ts index adc7c20..c165009 100644 --- a/src/db/queries/issues.ts +++ b/src/db/queries/issues.ts @@ -37,10 +37,19 @@ export async function updateIssue(id: number, updates: { title?: string; descrip return await db.update(Issue).set(updates).where(eq(Issue.id, id)).returning(); } +export async function getIssues() { + return await db.select().from(Issue); +} + export async function getIssuesByProject(projectId: number) { return await db.select().from(Issue).where(eq(Issue.projectId, projectId)); } +export async function getIssueByID(id: number) { + const [issue] = await db.select().from(Issue).where(eq(Issue.id, id)); + return issue; +} + export async function getIssueByNumber(projectId: number, number: number) { const [issue] = await db .select() diff --git a/src/index.ts b/src/index.ts index c3e1b94..963b329 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ const main = async () => { "/issue/create": routes.issueCreate, "/issue/update": routes.issueUpdate, "/issue/delete": routes.issueDelete, - "/issues/:projectId": routes.issues, + "/issues/:projectBlob": routes.issuesInProject, }, }); diff --git a/src/routes/index.ts b/src/routes/index.ts index 8513cde..6f6c455 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,11 +1,11 @@ -import issueCreate from "./issueCreate"; -import issueDelete from "./issueDelete"; -import issues from "./issues"; -import issueUpdate from "./issueUpdate"; +import issueCreate from "./issue/create"; +import issueDelete from "./issue/delete"; +import issueUpdate from "./issue/update"; +import issuesInProject from "./issues/[projectBlob]"; export const routes = { issueCreate, issueDelete, - issues, issueUpdate, + issuesInProject, }; diff --git a/src/routes/issueCreate.ts b/src/routes/issue/create.ts similarity index 98% rename from src/routes/issueCreate.ts rename to src/routes/issue/create.ts index 260cbb7..91cf0bd 100644 --- a/src/routes/issueCreate.ts +++ b/src/routes/issue/create.ts @@ -1,5 +1,5 @@ import type { BunRequest } from "bun"; -import { createIssue, getProjectByID, getProjectByBlob } from "../db/queries"; +import { createIssue, getProjectByID, getProjectByBlob } from "../../db/queries"; // /issue/create?projectId=1&title=Testing&description=Description // OR @@ -28,4 +28,3 @@ export default async function issueCreate(req: BunRequest) { return Response.json(issue); } - \ No newline at end of file diff --git a/src/routes/issueDelete.ts b/src/routes/issue/delete.ts similarity index 91% rename from src/routes/issueDelete.ts rename to src/routes/issue/delete.ts index 14ab0a5..68c2625 100644 --- a/src/routes/issueDelete.ts +++ b/src/routes/issue/delete.ts @@ -1,5 +1,5 @@ import type { BunRequest } from "bun"; -import { deleteIssue } from "../db/queries"; +import { deleteIssue } from "../../db/queries"; // /issue/delete?id=1 export default async function issueDelete(req: BunRequest) { diff --git a/src/routes/issueUpdate.ts b/src/routes/issue/update.ts similarity index 93% rename from src/routes/issueUpdate.ts rename to src/routes/issue/update.ts index a428a22..6893585 100644 --- a/src/routes/issueUpdate.ts +++ b/src/routes/issue/update.ts @@ -1,5 +1,5 @@ import type { BunRequest } from "bun"; -import { updateIssue } from "../db/queries"; +import { updateIssue } from "../../db/queries"; // /issue/update?id=1&title=Testing&description=Description export default async function issueUpdate(req: BunRequest) { diff --git a/src/routes/issues.ts b/src/routes/issues.ts deleted file mode 100644 index c70cdf9..0000000 --- a/src/routes/issues.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { BunRequest } from "bun"; -import { getIssuesByProject, getProjectByBlob } from "../db/queries"; - -export default async function issues(req: BunRequest<"/issues/:projectId">) { - const { projectId } = req.params; - - const project = await getProjectByBlob(projectId); - if (!project) { - return new Response("project not found", { status: 404 }); - } - const issues = await getIssuesByProject(project.id); - - return Response.json(issues); -} diff --git a/src/routes/issues/[projectBlob].ts b/src/routes/issues/[projectBlob].ts new file mode 100644 index 0000000..7dda5e1 --- /dev/null +++ b/src/routes/issues/[projectBlob].ts @@ -0,0 +1,14 @@ +import type { BunRequest } from "bun"; +import { getIssuesByProject, getProjectByBlob } from "../../db/queries"; + +export default async function issuesInProject(req: BunRequest<"/issues/:projectBlob">) { + const { projectBlob } = req.params; + + const project = await getProjectByBlob(projectBlob); + if (!project) { + return new Response(`project not found: provided ${projectBlob}`, { status: 404 }); + } + const issues = await getIssuesByProject(project.id); + + return Response.json(issues); +} From 3eeb7aeee735d59e3f1f4aa737942d552fa90ccc Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 01:00:03 +0000 Subject: [PATCH 20/22] /issues/all --- src/index.ts | 1 + src/routes/index.ts | 2 ++ src/routes/issues/all.ts | 8 ++++++++ 3 files changed, 11 insertions(+) create mode 100644 src/routes/issues/all.ts diff --git a/src/index.ts b/src/index.ts index 963b329..90a1a4c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ const main = async () => { "/issue/update": routes.issueUpdate, "/issue/delete": routes.issueDelete, "/issues/:projectBlob": routes.issuesInProject, + "/issues/all": routes.issues, }, }); diff --git a/src/routes/index.ts b/src/routes/index.ts index 6f6c455..690d5cc 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -2,10 +2,12 @@ import issueCreate from "./issue/create"; import issueDelete from "./issue/delete"; import issueUpdate from "./issue/update"; import issuesInProject from "./issues/[projectBlob]"; +import issues from "./issues/all"; export const routes = { issueCreate, issueDelete, issueUpdate, issuesInProject, + issues, }; diff --git a/src/routes/issues/all.ts b/src/routes/issues/all.ts new file mode 100644 index 0000000..9d97f45 --- /dev/null +++ b/src/routes/issues/all.ts @@ -0,0 +1,8 @@ +import type { BunRequest } from "bun"; +import { getIssues } from "../../db/queries"; + +export default async function issues(req: BunRequest) { + const issues = await getIssues(); + + return Response.json(issues); +} From ff7be7802ecc05ecb921b646011b815bb07f77b1 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 8 Dec 2025 01:50:13 +0000 Subject: [PATCH 21/22] /project/* routes --- src/db/queries/projects.ts | 24 +++++++++++++++----- src/db/queries/users.ts | 5 ++++ src/index.ts | 4 ++++ src/routes/index.ts | 9 ++++++++ src/routes/issues/all.ts | 2 +- src/routes/project/create.ts | 32 ++++++++++++++++++++++++++ src/routes/project/delete.ts | 21 +++++++++++++++++ src/routes/project/update.ts | 44 ++++++++++++++++++++++++++++++++++++ src/utils.ts | 5 +++- 9 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 src/routes/project/create.ts create mode 100644 src/routes/project/delete.ts create mode 100644 src/routes/project/update.ts diff --git a/src/db/queries/projects.ts b/src/db/queries/projects.ts index b81e2de..c4d12a9 100644 --- a/src/db/queries/projects.ts +++ b/src/db/queries/projects.ts @@ -1,22 +1,34 @@ import { eq } from "drizzle-orm"; import { db } from "../client"; -import { Project, User } from "../schema"; +import { Issue, Project, User } from "../schema"; -export async function createProject(blob: string, name: string, owner: typeof User.$inferSelect) { +export async function createProject(blob: string, name: string, ownerId: number) { const [project] = await db .insert(Project) .values({ blob, name, - ownerId: owner.id, + ownerId, }) .returning(); - if (!project) { - throw new Error(`failed to create project ${name} with blob ${blob} for owner ${owner.username}`); - } return project; } +export async function updateProject( + projectId: number, + updates: { blob?: string; name?: string; ownerId?: number }, +) { + const [project] = await db.update(Project).set(updates).where(eq(Project.id, projectId)).returning(); + return project; +} + +export async function deleteProject(projectId: number) { + // delete all of the project's issues first + await db.delete(Issue).where(eq(Issue.projectId, projectId)); + // delete actual project + await db.delete(Project).where(eq(Project.id, projectId)); +} + export async function getProjectByID(projectId: number) { const [project] = await db.select().from(Project).where(eq(Project.id, projectId)); return project; diff --git a/src/db/queries/users.ts b/src/db/queries/users.ts index 5372ff8..531de1e 100644 --- a/src/db/queries/users.ts +++ b/src/db/queries/users.ts @@ -7,6 +7,11 @@ export async function createUser(name: string, username: string) { return user; } +export async function getUserById(id: number) { + const [user] = await db.select().from(User).where(eq(User.id, id)); + return user; +} + export async function getUserByUsername(username: string) { const [user] = await db.select().from(User).where(eq(User.username, username)); return user; diff --git a/src/index.ts b/src/index.ts index 90a1a4c..2bdc454 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,10 @@ const main = async () => { "/issue/delete": routes.issueDelete, "/issues/:projectBlob": routes.issuesInProject, "/issues/all": routes.issues, + + "/project/create": routes.projectCreate, + "/project/update": routes.projectUpdate, + "/project/delete": routes.projectDelete, }, }); diff --git a/src/routes/index.ts b/src/routes/index.ts index 690d5cc..8cf93f9 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -4,10 +4,19 @@ import issueUpdate from "./issue/update"; import issuesInProject from "./issues/[projectBlob]"; import issues from "./issues/all"; +import projectCreate from "./project/create"; +import projectUpdate from "./project/update"; +import projectDelete from "./project/delete"; + export const routes = { issueCreate, issueDelete, issueUpdate, + issuesInProject, issues, + + projectCreate, + projectUpdate, + projectDelete, }; diff --git a/src/routes/issues/all.ts b/src/routes/issues/all.ts index 9d97f45..338f1a8 100644 --- a/src/routes/issues/all.ts +++ b/src/routes/issues/all.ts @@ -1,7 +1,7 @@ import type { BunRequest } from "bun"; import { getIssues } from "../../db/queries"; -export default async function issues(req: BunRequest) { +export default async function issuesAll(req: BunRequest) { const issues = await getIssues(); return Response.json(issues); diff --git a/src/routes/project/create.ts b/src/routes/project/create.ts new file mode 100644 index 0000000..2d1521e --- /dev/null +++ b/src/routes/project/create.ts @@ -0,0 +1,32 @@ +import type { BunRequest } from "bun"; +import { createProject, getUserById, getProjectByBlob } from "../../db/queries"; + +// /project/create?blob=BLOB&name=Testing&ownerId=1 +export default async function projectCreate(req: BunRequest) { + const url = new URL(req.url); + const blob = url.searchParams.get("blob"); + const name = url.searchParams.get("name"); + const ownerId = url.searchParams.get("ownerId"); + + if (!blob || !name || !ownerId) { + return new Response( + `missing parameters: ${!blob ? "blob " : ""}${!name ? "name " : ""}${!ownerId ? "ownerId" : ""}`, + { status: 400 }, + ); + } + + // check if project with blob already exists + const existingProject = await getProjectByBlob(blob); + if (existingProject) { + return new Response(`project with blob ${blob} already exists`, { status: 400 }); + } + + const owner = await getUserById(parseInt(ownerId, 10)); + if (!owner) { + return new Response(`owner with id ${ownerId} not found`, { status: 404 }); + } + + const project = await createProject(blob, name, owner.id); + + return Response.json(project); +} diff --git a/src/routes/project/delete.ts b/src/routes/project/delete.ts new file mode 100644 index 0000000..d58bd2d --- /dev/null +++ b/src/routes/project/delete.ts @@ -0,0 +1,21 @@ +import type { BunRequest } from "bun"; +import { getProjectByID, deleteProject } from "../../db/queries"; + +// /project/delete?id=1 +export default async function projectDelete(req: BunRequest) { + const url = new URL(req.url); + const id = url.searchParams.get("id"); + + if (!id) { + return new Response(`project id is required`, { status: 400 }); + } + + const existingProject = await getProjectByID(Number(id)); + if (!existingProject) { + return new Response(`project with id ${id} does not exist`, { status: 404 }); + } + + await deleteProject(Number(id)); + + return new Response(`project with id ${id} deleted successfully`, { status: 200 }); +} diff --git a/src/routes/project/update.ts b/src/routes/project/update.ts new file mode 100644 index 0000000..b1b817a --- /dev/null +++ b/src/routes/project/update.ts @@ -0,0 +1,44 @@ +import type { BunRequest } from "bun"; +import { getProjectByBlob, getProjectByID, getUserById, updateProject } from "../../db/queries"; + +// /project/update?id=1&blob=NEW&name=new%20name&ownerId=1 +export default async function projectUpdate(req: BunRequest) { + const url = new URL(req.url); + const id = url.searchParams.get("id"); + const blob = url.searchParams.get("blob") || undefined; + const name = url.searchParams.get("name") || undefined; + const ownerId = url.searchParams.get("ownerId") || undefined; + + if (!id) { + return new Response(`project id is required`, { status: 400 }); + } + + const existingProject = await getProjectByID(Number(id)); + if (!existingProject) { + return new Response(`project with id ${id} does not exist`, { status: 404 }); + } + + if (!blob && !name && !ownerId) { + return new Response(`at least one of blob, name, or ownerId must be provided`, { + status: 400, + }); + } + + const projectWithBlob = blob ? await getProjectByBlob(blob) : null; + if (projectWithBlob && projectWithBlob.id !== Number(id)) { + return new Response(`a project with blob "${blob}" already exists`, { status: 400 }); + } + + const newOwner = ownerId ? await getUserById(Number(ownerId)) : null; + if (ownerId && !newOwner) { + return new Response(`user with id ${ownerId} does not exist`, { status: 400 }); + } + + const project = await updateProject(Number(id), { + blob: blob, + name: name, + ownerId: newOwner?.id, + }); + + return Response.json(project); +} diff --git a/src/utils.ts b/src/utils.ts index d80b3de..ba7640b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,7 +8,10 @@ export const createDemoData = async () => { const projectNames = ["PROJ", "TEST", "SAMPLE"]; for (const name of projectNames) { - const project = await createProject(name.slice(0, 4), name, user); + const project = await createProject(name.slice(0, 4), name, user.id); + if (!project) { + throw new Error(`failed to create demo project: ${name}`); + } for (let i = 1; i <= 5; i++) { await createIssue( From acce648ee5e7e3a3006451e637c0db654820cc48 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Sat, 13 Dec 2025 18:14:55 +0000 Subject: [PATCH 22/22] CORS support CORS_ORIGIN environment variable --- .env.example | 5 ++- src/index.ts | 103 ++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 89 insertions(+), 19 deletions(-) diff --git a/.env.example b/.env.example index 5315d10..43ec90c 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,4 @@ -DATABASE_URL=postgres://eussi:password@localhost:5432/issue \ No newline at end of file +DATABASE_URL=postgres://eussi:password@localhost:5432/issue + +# comma separated list of allowed origins +CORS_ORIGIN=http://localhost:1420 \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 2bdc454..791a0d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,36 +6,103 @@ import { createDemoData } from "./utils"; const DEV = process.argv.find((arg) => ["--dev", "--developer", "-d"].includes(arg.toLowerCase())) != null; const PORT = process.argv.find((arg) => arg.toLowerCase().startsWith("--port="))?.split("=")[1] || 0; +type RouteHandler = (req: T) => Response | Promise; + +const CORS_ALLOWED_ORIGINS = (process.env.CORS_ORIGIN ?? "http://localhost:1420") + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean); + +const CORS_ALLOW_METHODS = process.env.CORS_ALLOW_METHODS ?? "GET,POST,PUT,PATCH,DELETE,OPTIONS"; +const CORS_ALLOW_HEADERS_DEFAULT = process.env.CORS_ALLOW_HEADERS ?? "Content-Type, Authorization"; +const CORS_MAX_AGE = process.env.CORS_MAX_AGE ?? "86400"; + +const getCorsAllowOrigin = (req: Request) => { + const requestOrigin = req.headers.get("Origin"); + if (!requestOrigin) { + return "*"; + } + + if (CORS_ALLOWED_ORIGINS.includes("*")) { + return "*"; + } + + if (CORS_ALLOWED_ORIGINS.includes(requestOrigin)) { + return requestOrigin; + } + + return null; +}; + +const buildCorsHeaders = (req: Request) => { + const headers = new Headers(); + + const allowOrigin = getCorsAllowOrigin(req); + if (allowOrigin) { + headers.set("Access-Control-Allow-Origin", allowOrigin); + if (allowOrigin !== "*") { + headers.set("Vary", "Origin"); + } + } + + headers.set("Access-Control-Allow-Methods", CORS_ALLOW_METHODS); + + const requestedHeaders = req.headers.get("Access-Control-Request-Headers"); + headers.set("Access-Control-Allow-Headers", requestedHeaders || CORS_ALLOW_HEADERS_DEFAULT); + + headers.set("Access-Control-Max-Age", CORS_MAX_AGE); + + return headers; +}; + +const withCors = (handler: RouteHandler): RouteHandler => { + return async (req: T) => { + const corsHeaders = buildCorsHeaders(req); + + if (req.method === "OPTIONS") { + return new Response(null, { status: 204, headers: corsHeaders }); + } + + const res = await handler(req); + const wrapped = new Response(res.body, res); + + corsHeaders.forEach((value, key) => { + wrapped.headers.set(key, value); + }); + + return wrapped; + }; +}; + const main = async () => { const server = Bun.serve({ port: Number(PORT), routes: { - "/": () => new Response(`title: eussi\ndev-mode: ${DEV}\nport: ${PORT}`), - "/issue/create": routes.issueCreate, - "/issue/update": routes.issueUpdate, - "/issue/delete": routes.issueDelete, - "/issues/:projectBlob": routes.issuesInProject, - "/issues/all": routes.issues, + "/": withCors(() => new Response(`title: eussi\ndev-mode: ${DEV}\nport: ${PORT}`)), - "/project/create": routes.projectCreate, - "/project/update": routes.projectUpdate, - "/project/delete": routes.projectDelete, + "/issue/create": withCors(routes.issueCreate), + "/issue/update": withCors(routes.issueUpdate), + "/issue/delete": withCors(routes.issueDelete), + "/issues/:projectBlob": withCors(routes.issuesInProject), + "/issues/all": withCors(routes.issues), + + "/project/create": withCors(routes.projectCreate), + "/project/update": withCors(routes.projectUpdate), + "/project/delete": withCors(routes.projectDelete), }, }); console.log(`eussi (issue server) listening on ${server.url}`); await testDB(); - let users = await db.select().from(User); - - if (DEV && users.length === 0) { - console.log("creating demo data..."); - await createDemoData(); - console.log("demo data created"); - users = await db.select().from(User); + if (DEV) { + const users = await db.select().from(User); + if (users.length === 0) { + console.log("creating demo data..."); + await createDemoData(); + console.log("demo data created"); + } } - - console.log(`serving ${users.length} user${users.length === 1 ? "" : "s"}`); }; main();