M0: foundation — monorepo, PostGIS schema, deck query, app shell
Greenfield scaffold for Linkder, a swipe-to-hire marketplace for local
professional services.
- pnpm/turbo monorepo: apps/web, packages/{shared,db}
- Postgres 16 + PostGIS via docker compose (ports 5442/6389 to avoid
clashing with other local stacks)
- Drizzle schema, 23 tables, geography(Point,4326) with GiST indexes
- Domain core in packages/shared: integer-cent money, status transition
graphs, deck ranking weights, cancellation policy — 46 unit tests
- Deck query: filtering in Postgres on the GiST index, ranking in JS so
the weights stay tunable — 18 integration tests against a seeded DB
- Deterministic seed placing pros at known distances, including three
that must NOT appear on a deck (out of radius, unverified, away)
- Next.js 15 app shell with a working swipe deck
- CI: typecheck, lint, test, build against live postgres+redis
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import * as schema from './schema/index';
|
||||
|
||||
/**
|
||||
* The pool and the Drizzle instance are created on first use, not on import.
|
||||
*
|
||||
* Eager construction would make `next build` fail on any machine without a
|
||||
* database — including CI, which only needs to compile pages. Failing on first
|
||||
* query instead keeps the failure where it is actionable.
|
||||
*
|
||||
* The instance is cached on globalThis so Next's dev server does not open a new
|
||||
* pool on every hot reload and exhaust Postgres connections within a minute.
|
||||
*/
|
||||
const globalForDb = globalThis as unknown as {
|
||||
__linkderPool?: postgres.Sql;
|
||||
__linkderDb?: PostgresJsDatabase<typeof schema>;
|
||||
};
|
||||
|
||||
function createPool(): postgres.Sql {
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
if (!connectionString) {
|
||||
throw new Error(
|
||||
'DATABASE_URL is not set. Copy .env.example to .env and start Postgres with `pnpm services:up`.',
|
||||
);
|
||||
}
|
||||
return postgres(connectionString, {
|
||||
max: Number(process.env.DB_POOL_MAX ?? 10),
|
||||
idle_timeout: 20,
|
||||
});
|
||||
}
|
||||
|
||||
export function getPool(): postgres.Sql {
|
||||
const existing = globalForDb.__linkderPool;
|
||||
if (existing) return existing;
|
||||
const created = createPool();
|
||||
globalForDb.__linkderPool = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
function getDb(): PostgresJsDatabase<typeof schema> {
|
||||
const existing = globalForDb.__linkderDb;
|
||||
if (existing) return existing;
|
||||
const created = drizzle(getPool(), { schema });
|
||||
globalForDb.__linkderDb = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
export type Db = PostgresJsDatabase<typeof schema>;
|
||||
|
||||
/**
|
||||
* Lazily-initialised database handle. Behaves exactly like a Drizzle instance;
|
||||
* the connection is only opened when a property is first touched.
|
||||
*/
|
||||
export const db: Db = new Proxy({} as Db, {
|
||||
get(_target, prop, receiver) {
|
||||
return Reflect.get(getDb() as object, prop, receiver);
|
||||
},
|
||||
has(_target, prop) {
|
||||
return Reflect.has(getDb() as object, prop);
|
||||
},
|
||||
});
|
||||
|
||||
/** Close the pool. For scripts and test teardown — never call this from a request. */
|
||||
export async function closePool(): Promise<void> {
|
||||
const existing = globalForDb.__linkderPool;
|
||||
if (!existing) return;
|
||||
await existing.end();
|
||||
globalForDb.__linkderPool = undefined;
|
||||
globalForDb.__linkderDb = undefined;
|
||||
}
|
||||
|
||||
export { schema };
|
||||
Reference in New Issue
Block a user