Compare commits
5
Commits
v1.0.0
..
e96c40c9c7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e96c40c9c7 | ||
|
|
2d20f5e893 | ||
|
|
b395a0f98c | ||
|
|
ae88415406 | ||
|
|
37787ff0dd |
@@ -57,3 +57,23 @@ this directory:
|
||||
Deployed to Fly.io (`fly.toml`). After a domain changes, re-run
|
||||
`src/scripts/register-webhooks.ts` with production env vars to point Printful's
|
||||
and Webflow's webhooks at the new domain — see issue #3 for the full checklist.
|
||||
|
||||
### Running migrations/seed against production
|
||||
|
||||
The deployed image only contains the compiled binary (see `Dockerfile`) — no
|
||||
source, no `bun_modules`, no migration files — so these can't be run from
|
||||
`fly ssh console` on the API app itself. Instead, use `migrate.sh` at the repo
|
||||
root, which tunnels to the Postgres app (`blade-and-brawn-db`, legacy/unmanaged
|
||||
Fly Postgres) via `fly proxy`, fetches the production `DATABASE_URL` for you,
|
||||
and runs the scripts against it:
|
||||
|
||||
```bash
|
||||
./migrate.sh migrate # db:migrate:latest (default if no argument given)
|
||||
./migrate.sh seed # db:seed
|
||||
./migrate.sh both # both, in order
|
||||
```
|
||||
|
||||
Both underlying scripts prompt for a `y/N` confirmation before touching the
|
||||
database, and `db:seed` is idempotent (skips seeding if the default rows
|
||||
already exist). If the tunneled connection fails on TLS, legacy Postgres
|
||||
sometimes needs `?sslmode=disable` appended — edit `migrate.sh` if so.
|
||||
|
||||
@@ -27,11 +27,11 @@
|
||||
"@elysia/jwt": "^1.4.2",
|
||||
"@elysia/server-timing": "^1.4.1",
|
||||
"@elysiajs/cors": "^1.4.2",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/pg": "^8.21.0",
|
||||
"elysia": "^1.4.29",
|
||||
"kysely": "^0.29.4",
|
||||
"kysely": "^0.29.5",
|
||||
"ml-levenberg-marquardt": "^5.1.0",
|
||||
"pg": "^8.22.0",
|
||||
"pg": "^8.23.0",
|
||||
"zipcodes-us": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Kysely, sql } from 'kysely'
|
||||
import { addDefaultColumns } from '../db';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
// TABLE: ACCOUNTS
|
||||
await db.schema.createTable("accounts")
|
||||
.addColumn("id", "uuid", (cb) => cb.primaryKey().defaultTo(sql`uuidv7()`))
|
||||
.addColumn("created_at", "timestamptz", (cb) => cb
|
||||
.notNull()
|
||||
.defaultTo(sql`now()`)
|
||||
)
|
||||
.addColumn("discord_id", "text", (cb) => cb.unique())
|
||||
.addColumn("email", "text", (cb) => cb.unique())
|
||||
.addColumn("name", "text", (cb) => cb.notNull().defaultTo("Anonymous"))
|
||||
.addColumn("gender", "text")
|
||||
.execute();
|
||||
|
||||
// TABLE: ASSESSMENTS
|
||||
await db.schema.createTable("assessments")
|
||||
.$call(addDefaultColumns)
|
||||
.addColumn("account_id", "uuid")
|
||||
.addColumn("name", "text", (cb) => cb.notNull().defaultTo("Anonymous"))
|
||||
.addColumn("gender", "text", (cb) => cb.notNull())
|
||||
.addColumn("age", "integer", (cb) => cb.notNull())
|
||||
.addColumn("weight", "float8", (cb) => cb.notNull())
|
||||
.addColumn("perf_back_squat", "float8") // kg
|
||||
.addColumn("perf_deadlift", "float8") // kg
|
||||
.addColumn("perf_bench_press", "float8") // kg
|
||||
.addColumn("perf_run", "float8") // ms
|
||||
.addColumn("perf_broad_jump", "float8") // cm
|
||||
.addColumn("perf_cone_drill", "float8") // ms
|
||||
.addForeignKeyConstraint(
|
||||
"fk_assessments_account_id",
|
||||
["account_id"],
|
||||
"accounts",
|
||||
["id"],
|
||||
(cb) => cb.onDelete("cascade")
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
// TABLE: ASSESSMENTS
|
||||
await db.schema.dropTable("assessments").ifExists().execute()
|
||||
// TABLE: ACCOUNTS
|
||||
await db.schema.dropTable("accounts").ifExists().execute()
|
||||
}
|
||||
+25
-2
@@ -20,10 +20,11 @@ import { CalculatorService, CalculatorUnavailableError } from "./services/calcul
|
||||
import { StandardsParamsSchema } from "@blade-and-brawn/calculator";
|
||||
import { StandardsService } from "./services/standards";
|
||||
import { EventsService, EventStatusSchema } from "./services/events";
|
||||
import { AccountsService } from "./services/accounts";
|
||||
|
||||
// CONSTANTS
|
||||
// -----------------------
|
||||
const EVENT_QUEUE_MANAGE_DELAY_MS = 1000 // 1 sec
|
||||
const EVENT_QUEUE_MANAGE_DELAY_MS = 1000 // 1 sec
|
||||
const JWT_EXP = "1d";
|
||||
|
||||
// SERVICES
|
||||
@@ -32,8 +33,9 @@ const s = (() => {
|
||||
const Standards = new StandardsService();
|
||||
const Calculator = new CalculatorService(DEFAULT_NAME);
|
||||
const Commerce = new CommerceService();
|
||||
const Accounts = new AccountsService(Calculator);
|
||||
const Events = new EventsService();
|
||||
return { Standards, Calculator, Commerce, Events };
|
||||
return { Standards, Calculator, Commerce, Accounts, Events };
|
||||
})();
|
||||
|
||||
// QUEUES
|
||||
@@ -337,6 +339,27 @@ export const app = new Elysia()
|
||||
.get("/groups", async ({ }) => queues.map((q) => q.group))
|
||||
)
|
||||
|
||||
// ACCOUNTS
|
||||
.group("/accounts", (app) => app
|
||||
// Non-authenticated
|
||||
.get("/:id/stats", async ({ params: { id } }) => {
|
||||
const stats = await s.Accounts.stats(id);
|
||||
if (!stats) throw new NotFoundError("Account stats not found");
|
||||
return stats;
|
||||
}, {
|
||||
params: t.Object({ id: t.String() })
|
||||
})
|
||||
// Authenticated
|
||||
.guard({ auth: true })
|
||||
.get("/:id", async ({ params: { id } }) => {
|
||||
const account = await s.Accounts.get(id);
|
||||
if (!account) throw new NotFoundError("Account not found");
|
||||
return account;
|
||||
}, {
|
||||
params: t.Object({ id: t.String() })
|
||||
})
|
||||
)
|
||||
|
||||
// WEBHOOKS
|
||||
.post("/webhooks/printful", async ({ body, query }) => {
|
||||
// https://webflow.com/integrations/printful
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Activity, Gender, type Player } from "@blade-and-brawn/domain";
|
||||
import { db } from "../database/db";
|
||||
import type { CalculatorService } from "./calculator";
|
||||
import { Value } from "@sinclair/typebox/value";
|
||||
import { t } from "elysia";
|
||||
|
||||
export class AccountsService {
|
||||
constructor(private Calculator: CalculatorService) { }
|
||||
|
||||
async get(id: string) {
|
||||
return await (db).selectFrom("accounts")
|
||||
.selectAll()
|
||||
.where("id", "=", id)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async stats(id: string) {
|
||||
const latestAssessment = await db.selectFrom("accounts")
|
||||
.innerJoin("assessments", "assessments.account_id", "accounts.id")
|
||||
.select([
|
||||
"assessments.age", "assessments.gender", "assessments.weight", "assessments.name",
|
||||
"assessments.perf_back_squat", "assessments.perf_bench_press", "assessments.perf_broad_jump",
|
||||
"assessments.perf_cone_drill", "assessments.perf_deadlift", "assessments.perf_run"
|
||||
])
|
||||
.where("accounts.id", "=", id)
|
||||
.orderBy("assessments.created_at", "desc")
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
if (!latestAssessment) return;
|
||||
Value.Assert(t.Enum(Gender), latestAssessment.gender);
|
||||
|
||||
return this.Calculator.calculate(
|
||||
{
|
||||
name: latestAssessment.name,
|
||||
metrics: {
|
||||
age: latestAssessment.age,
|
||||
weight: latestAssessment.weight,
|
||||
gender: latestAssessment.gender
|
||||
}
|
||||
},
|
||||
[
|
||||
// Strength
|
||||
{
|
||||
activity: Activity.BenchPress,
|
||||
performance: latestAssessment.perf_bench_press ?? 0
|
||||
},
|
||||
{
|
||||
activity: Activity.Deadlift,
|
||||
performance: latestAssessment.perf_deadlift ?? 0
|
||||
},
|
||||
{
|
||||
activity: Activity.BackSquat,
|
||||
performance: latestAssessment.perf_back_squat ?? 0
|
||||
},
|
||||
// Power
|
||||
{
|
||||
activity: Activity.BroadJump,
|
||||
performance: latestAssessment.perf_broad_jump ?? 0
|
||||
},
|
||||
// Endurance
|
||||
{
|
||||
activity: Activity.Run,
|
||||
performance: latestAssessment.perf_run ?? 0
|
||||
},
|
||||
// Agility
|
||||
{
|
||||
activity: Activity.ConeDrill,
|
||||
performance: latestAssessment.perf_cone_drill ?? 0
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/bun": "^1.3.14",
|
||||
"elysia": "^1.4.29",
|
||||
"svelte": "^5.56.8",
|
||||
"svelte": "^5.56.9",
|
||||
"svelte-adapter-bun": "^1.0.1",
|
||||
"svelte-check": "^4.7.4",
|
||||
"svelte-check": "^4.7.6",
|
||||
"vite": "^7.3.6"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -26,8 +26,8 @@
|
||||
"@blade-and-brawn/domain": "workspace:*",
|
||||
"@elysia/eden": "^1.4.10",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"daisyui": "^5.7.9",
|
||||
"jose": "^6.2.5",
|
||||
"daisyui": "^5.7.17",
|
||||
"jose": "^6.2.9",
|
||||
"tailwindcss": "^4.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
"@elysia/jwt": "^1.4.2",
|
||||
"@elysia/server-timing": "^1.4.1",
|
||||
"@elysiajs/cors": "^1.4.2",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/pg": "^8.21.0",
|
||||
"elysia": "^1.4.29",
|
||||
"kysely": "^0.29.4",
|
||||
"kysely": "^0.29.5",
|
||||
"ml-levenberg-marquardt": "^5.1.0",
|
||||
"pg": "^8.22.0",
|
||||
"pg": "^8.23.0",
|
||||
"zipcodes-us": "^1.1.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -42,8 +42,8 @@
|
||||
"@blade-and-brawn/domain": "workspace:*",
|
||||
"@elysia/eden": "^1.4.10",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"daisyui": "^5.7.9",
|
||||
"jose": "^6.2.5",
|
||||
"daisyui": "^5.7.17",
|
||||
"jose": "^6.2.9",
|
||||
"tailwindcss": "^4.3.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -52,9 +52,9 @@
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/bun": "^1.3.14",
|
||||
"elysia": "^1.4.29",
|
||||
"svelte": "^5.56.8",
|
||||
"svelte": "^5.56.9",
|
||||
"svelte-adapter-bun": "^1.0.1",
|
||||
"svelte-check": "^4.7.4",
|
||||
"svelte-check": "^4.7.6",
|
||||
"vite": "^7.3.6",
|
||||
},
|
||||
},
|
||||
@@ -272,7 +272,7 @@
|
||||
|
||||
"@sveltejs/kit": ["@sveltejs/kit@2.70.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-RzRoRpuR2KXqc5yMO0akQHDZeT4AslOlznGITURsqHaVbtyYP4Wn3eE3gxj9JcDyNYO0crkxhdwFHc+2vkVm6w=="],
|
||||
|
||||
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.1", "", {}, "sha512-5m3B2cbqQ4TbwW6Xkh66Ntw6dD7gNc77cCxABTTesWcq9jxIzMgTk97pZx5vEtvQx8iokgi7GIphqZe+PGwcZA=="],
|
||||
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
||||
|
||||
@@ -322,7 +322,7 @@
|
||||
|
||||
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||
|
||||
"@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="],
|
||||
"@types/pg": ["@types/pg@8.21.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-AYdtudzabjLZgVgRZmAnU8bAnVUXzuJX2IYHeSIiIHm68olD+LgQYCGWdtcNYnP0uq9c4S4NibVG3Ni7VbKW7Q=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
|
||||
"cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="],
|
||||
|
||||
"daisyui": ["daisyui@5.7.9", "", {}, "sha512-oPL7yddYPQrMsDmYtxNqPGBYc+gqm14GTwu7cDxAaUq0bPg5ZcaeR/DVMgSwgo26InVn8AECbUBUCK0rMs96Kg=="],
|
||||
"daisyui": ["daisyui@5.7.17", "", {}, "sha512-w5U5FdL9hH4/zh8eLXytWQt3c0DgHS1WDw+1yuQykT8nSIE9Vc5wkJVeDymD1JlRDLANDawvN4iIGVaxgV6Cjw=="],
|
||||
|
||||
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
|
||||
|
||||
@@ -470,7 +470,7 @@
|
||||
|
||||
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||
|
||||
"jose": ["jose@6.2.5", "", {}, "sha512-2E5L2yRp03FnwreJLJX8/r7mHiZICCf8kG7fAsTWkSQTDAcc46NIZoQLKy+EJ8sPoJlxyS4OQR5H70LjIZZlIQ=="],
|
||||
"jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="],
|
||||
|
||||
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
|
||||
|
||||
@@ -482,7 +482,7 @@
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"kysely": ["kysely@0.29.4", "", {}, "sha512-y5mVgQNkMbs1eK9Xyc0pmNdabN2wHhRYY/5r4W5HrUT1rYCEPeVNSj1RUJeSDKT3U0p+mXCvLgkrFuIafYI6BA=="],
|
||||
"kysely": ["kysely@0.29.5", "", {}, "sha512-ooa+eSbBNPTo3MycPEuW5jdrxQdQwdtB3LC3h43FiXQbIry5tR0C5lDG7eealK0E4D7XjrnOP5DIUg/LyjRMYQ=="],
|
||||
|
||||
"kysely-codegen": ["kysely-codegen@0.20.0", "", { "dependencies": { "chalk": "4.1.2", "cosmiconfig": "^9.0.0", "diff": "^8.0.3", "dotenv": "^17.2.4", "dotenv-expand": "^12.0.3", "micromatch": "^4.0.8", "minimist": "^1.2.8", "pluralize": "^8.0.0", "zod": "^4.3.6" }, "peerDependencies": { "@libsql/kysely-libsql": ">=0.3.0 <0.5.0", "@tediousjs/connection-string": "^1.0.0", "better-sqlite3": ">=7.6.2 <13.0.0", "kysely": ">=0.27.0 <1.0.0", "kysely-bun-sqlite": ">=0.3.2 <1.0.0", "kysely-bun-worker": ">=1.2.0 <2.0.0", "mysql2": ">=2.3.3 <4.0.0", "pg": ">=8.8.0 <9.0.0", "tarn": ">=3.0.0 <4.0.0", "tedious": ">=18.0.0 <20.0.0" }, "optionalPeers": ["@libsql/kysely-libsql", "@tediousjs/connection-string", "better-sqlite3", "kysely-bun-sqlite", "kysely-bun-worker", "mysql2", "pg", "tarn", "tedious"], "bin": { "kysely-codegen": "dist/cli/bin.js" } }, "sha512-LSi2KBG7uDmNCZ+XurLSA9LH7XFyyoQ6xb5DLJSInPTSYLVApjOP2KwO8mSaREWTtoX+C2AG2GTlYR0DLjTbcA=="],
|
||||
|
||||
@@ -552,7 +552,7 @@
|
||||
|
||||
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
|
||||
|
||||
"pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="],
|
||||
"pg": ["pg@8.23.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.16.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
|
||||
|
||||
@@ -630,11 +630,11 @@
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"svelte": ["svelte@5.56.8", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg=="],
|
||||
"svelte": ["svelte@5.56.9", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-VT8kSnlEg8069w7AiCcAk3Yf5xvMnrGTagVOmU/OpOLHaHnNqXhWZCH/4EVga/bT/HtWhvE6/fHrXLErx7OnJA=="],
|
||||
|
||||
"svelte-adapter-bun": ["svelte-adapter-bun@1.0.1", "", { "dependencies": { "rolldown": "^1.0.0-beta.38" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0", "typescript": "^5" } }, "sha512-tNOvfm8BGgG+rmEA7hkmqtq07v7zoo4skLQc+hIoQ79J+1fkEMpJEA2RzCIe3aPc8JdrsMJkv3mpiZPMsgahjA=="],
|
||||
|
||||
"svelte-check": ["svelte-check@4.7.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.1", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-IW9ot9YqAoyv8FvyN+eb4ZTe8zgcKZrJLNYU6dzSKkGwEBsSPc4K7lmQ8bKn8W2YMXM6WDfZSSVOaGtekyUfOQ=="],
|
||||
"svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||
|
||||
@@ -692,6 +692,8 @@
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"pg/pg-protocol": ["pg-protocol@1.16.0", "", {}, "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg=="],
|
||||
|
||||
"thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="],
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
PG_APP="blade-and-brawn-db"
|
||||
API_APP="blade-and-brawn-api"
|
||||
LOCAL_PORT=5433
|
||||
|
||||
proxy_pid=""
|
||||
cleanup() {
|
||||
if [[ -n "$proxy_pid" ]]; then
|
||||
kill "$proxy_pid" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "Fetching production DATABASE_URL from $API_APP..." >&2
|
||||
prod_url=$(fly ssh console -a "$API_APP" -C "printenv DATABASE_URL" 2>/dev/null | grep -m1 '^postgres')
|
||||
if [[ -z "$prod_url" ]]; then
|
||||
echo "Failed to fetch DATABASE_URL from $API_APP" >&2
|
||||
exit 1
|
||||
fi
|
||||
tunneled_url=$(echo "$prod_url" | sed -E "s#@[^/]+/#@localhost:${LOCAL_PORT}/#")
|
||||
|
||||
echo "Starting tunnel to $PG_APP on localhost:${LOCAL_PORT}..." >&2
|
||||
fly proxy "${LOCAL_PORT}:5432" -a "$PG_APP" &
|
||||
proxy_pid=$!
|
||||
|
||||
echo "Waiting for tunnel..." >&2
|
||||
for _ in $(seq 1 30); do
|
||||
nc -z localhost "$LOCAL_PORT" 2>/dev/null && break
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
target="${1:-migrate}"
|
||||
|
||||
case "$target" in
|
||||
migrate)
|
||||
DATABASE_URL="$tunneled_url" bun run --filter @blade-and-brawn/api db:migrate:latest
|
||||
;;
|
||||
seed)
|
||||
DATABASE_URL="$tunneled_url" bun run --filter @blade-and-brawn/api db:seed
|
||||
;;
|
||||
both)
|
||||
DATABASE_URL="$tunneled_url" bun run --filter @blade-and-brawn/api db:migrate:latest
|
||||
DATABASE_URL="$tunneled_url" bun run --filter @blade-and-brawn/api db:seed
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [migrate|seed|both]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user