converted to proper npm sveltekit project
This commit is contained in:
27
src/lib/server/db/index.ts
Normal file
27
src/lib/server/db/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// Postgres connection pool. Created lazily on first use so the module can be
|
||||
// imported during build/analysis without DATABASE_URL being present.
|
||||
import pg from 'pg';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
let pool: pg.Pool | null = null;
|
||||
|
||||
function getPool(): pg.Pool {
|
||||
if (pool) return pool;
|
||||
const connectionString = env.DATABASE_URL;
|
||||
if (!connectionString) {
|
||||
// Fail loudly on first query rather than mysteriously later.
|
||||
throw new Error(
|
||||
'DATABASE_URL is not set. Copy .env.example to .env (or set it in docker-compose) and try again.'
|
||||
);
|
||||
}
|
||||
pool = new pg.Pool({ connectionString });
|
||||
return pool;
|
||||
}
|
||||
|
||||
/** Small helper so call sites read as `query(sql, params)`. */
|
||||
export function query<T extends pg.QueryResultRow = pg.QueryResultRow>(
|
||||
text: string,
|
||||
params?: unknown[]
|
||||
): Promise<pg.QueryResult<T>> {
|
||||
return getPool().query<T>(text, params);
|
||||
}
|
||||
107
src/lib/server/db/workspaces.ts
Normal file
107
src/lib/server/db/workspaces.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
// Repository for reading and writing tool workspaces.
|
||||
//
|
||||
// Concurrency model: optimistic locking on an integer `version`. A client loads
|
||||
// a workspace (getting its version), edits locally, then saves with that same
|
||||
// version as `baseVersion`. If nobody else saved in between, the row's version
|
||||
// still matches and the write wins (bumping version). If someone else saved
|
||||
// first, the versions differ, no row updates, and we report a conflict so the
|
||||
// client can reload instead of clobbering their teammate.
|
||||
|
||||
import { query } from './index';
|
||||
import { defaultStateFor } from '$lib/defaults';
|
||||
import type { ToolId, WorkspaceEnvelope } from '$lib/types';
|
||||
|
||||
interface Row {
|
||||
tool: ToolId;
|
||||
name: string;
|
||||
state: unknown;
|
||||
version: number;
|
||||
updated_at: Date;
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
function toEnvelope<T>(row: Row): WorkspaceEnvelope<T> {
|
||||
return {
|
||||
tool: row.tool,
|
||||
name: row.name,
|
||||
state: row.state as T,
|
||||
version: row.version,
|
||||
updatedAt: row.updated_at.toISOString(),
|
||||
updatedBy: row.updated_by
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a workspace. If it does not exist yet, seed it from the tool's default
|
||||
* baseline state so the first visitor sees a populated, useful tool.
|
||||
*/
|
||||
export async function getOrCreateWorkspace<T>(
|
||||
tool: ToolId,
|
||||
name: string
|
||||
): Promise<WorkspaceEnvelope<T>> {
|
||||
const existing = await query<Row>(
|
||||
'SELECT tool, name, state, version, updated_at, updated_by FROM workspaces WHERE tool = $1 AND name = $2',
|
||||
[tool, name]
|
||||
);
|
||||
if (existing.rows.length > 0) {
|
||||
return toEnvelope<T>(existing.rows[0]);
|
||||
}
|
||||
|
||||
const seed = defaultStateFor(tool);
|
||||
// ON CONFLICT guards against a race where two first-visitors insert at once.
|
||||
const inserted = await query<Row>(
|
||||
`INSERT INTO workspaces (tool, name, state)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (tool, name) DO UPDATE SET tool = EXCLUDED.tool
|
||||
RETURNING tool, name, state, version, updated_at, updated_by`,
|
||||
[tool, name, seed]
|
||||
);
|
||||
return toEnvelope<T>(inserted.rows[0]);
|
||||
}
|
||||
|
||||
export type SaveResult<T> =
|
||||
| { ok: true; workspace: WorkspaceEnvelope<T> }
|
||||
| { ok: false; conflict: WorkspaceEnvelope<T> };
|
||||
|
||||
/** Save state with optimistic concurrency. */
|
||||
export async function saveWorkspace<T>(
|
||||
tool: ToolId,
|
||||
name: string,
|
||||
state: T,
|
||||
baseVersion: number,
|
||||
updatedBy: string | null
|
||||
): Promise<SaveResult<T>> {
|
||||
const updated = await query<Row>(
|
||||
`UPDATE workspaces
|
||||
SET state = $3, version = version + 1, updated_at = now(), updated_by = $5
|
||||
WHERE tool = $1 AND name = $2 AND version = $4
|
||||
RETURNING tool, name, state, version, updated_at, updated_by`,
|
||||
[tool, name, state, baseVersion, updatedBy]
|
||||
);
|
||||
|
||||
if (updated.rows.length === 1) {
|
||||
return { ok: true, workspace: toEnvelope<T>(updated.rows[0]) };
|
||||
}
|
||||
|
||||
// No row updated: either the workspace does not exist, or the version moved.
|
||||
// Return the current server copy so the client can reconcile.
|
||||
const current = await getOrCreateWorkspace<T>(tool, name);
|
||||
return { ok: false, conflict: current };
|
||||
}
|
||||
|
||||
/** Reset a workspace back to the tool's baseline. */
|
||||
export async function resetWorkspace<T>(
|
||||
tool: ToolId,
|
||||
name: string
|
||||
): Promise<WorkspaceEnvelope<T>> {
|
||||
const seed = defaultStateFor(tool);
|
||||
const reset = await query<Row>(
|
||||
`INSERT INTO workspaces (tool, name, state)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (tool, name)
|
||||
DO UPDATE SET state = EXCLUDED.state, version = workspaces.version + 1, updated_at = now(), updated_by = NULL
|
||||
RETURNING tool, name, state, version, updated_at, updated_by`,
|
||||
[tool, name, seed]
|
||||
);
|
||||
return toEnvelope<T>(reset.rows[0]);
|
||||
}
|
||||
Reference in New Issue
Block a user