179 lines
4.3 KiB
TypeScript
179 lines
4.3 KiB
TypeScript
import {
|
|
Pool,
|
|
PoolClient,
|
|
PostgresError,
|
|
} from "https://deno.land/x/postgres@v0.16.1/mod.ts";
|
|
import {
|
|
type QueryArguments,
|
|
type QueryArrayResult,
|
|
type QueryObjectResult,
|
|
} from "https://deno.land/x/postgres@v0.16.1/query/query.ts?s=QueryArguments";
|
|
import { config } from "@/config.ts";
|
|
|
|
import {
|
|
type Identifiable,
|
|
type Note,
|
|
type Team,
|
|
type Timestamped,
|
|
type User,
|
|
} from "@/types.ts";
|
|
|
|
export { PostgresError };
|
|
export { type QueryObjectResult };
|
|
|
|
const pool = new Pool(config.postgres.url, 3, true);
|
|
|
|
async function dbOp<T>(op: (connection: PoolClient) => Promise<T>) {
|
|
let result = null;
|
|
let exception = null;
|
|
try {
|
|
const connection = await pool.connect();
|
|
try {
|
|
result = await op(connection);
|
|
} catch (err) {
|
|
console.error("Error querying database:", { ...err });
|
|
exception = err;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
} catch (err) {
|
|
exception = err;
|
|
console.error("Error connecting to database:", err);
|
|
}
|
|
if (exception != null) throw exception;
|
|
return result;
|
|
}
|
|
|
|
export async function queryObject<T>(
|
|
sql: string,
|
|
args?: QueryArguments,
|
|
): Promise<QueryObjectResult<T> | null> {
|
|
return await dbOp(async (connection) =>
|
|
await connection.queryObject<T>({
|
|
camelcase: true,
|
|
text: sql.trim(),
|
|
args,
|
|
})
|
|
);
|
|
}
|
|
|
|
export async function queryArray<T extends []>(
|
|
sql: string,
|
|
args?: QueryArguments,
|
|
): Promise<QueryArrayResult<T> | null> {
|
|
return await dbOp(async (connection) =>
|
|
await connection.queryArray<T>({
|
|
text: sql.trim(),
|
|
args,
|
|
})
|
|
);
|
|
}
|
|
|
|
export async function listNotes(): Promise<Note[] | null> {
|
|
return someRows(
|
|
await queryObject<Note>(
|
|
"select * from note order by created_at desc",
|
|
),
|
|
);
|
|
}
|
|
|
|
export async function getNote(
|
|
id: string | { id: string },
|
|
): Promise<Note | null> {
|
|
const idVal = typeof id == "object" ? id.id : id;
|
|
console.debug("getNote id =", JSON.stringify(idVal));
|
|
return singleRow(
|
|
await queryObject<Note>(
|
|
"select * from note where id = $1",
|
|
[idVal],
|
|
),
|
|
);
|
|
}
|
|
|
|
type Ungenerated<T> = Omit<T, keyof Identifiable | keyof Timestamped>;
|
|
|
|
export async function createNote(
|
|
{ content }: Ungenerated<Note>,
|
|
): Promise<Note | null> {
|
|
return singleRow(
|
|
await queryObject<Note>(
|
|
"insert into note (content) values ($1) returning *",
|
|
[content],
|
|
),
|
|
);
|
|
}
|
|
|
|
export async function createUser(
|
|
{ username, passwordDigest }: Ungenerated<User>,
|
|
): Promise<[User | null, Team | null] | null> {
|
|
const result = singleRow(
|
|
await queryObject<{ teamId: string; userId: string }>(
|
|
`
|
|
with new_user as (
|
|
insert into "user" (username, password_digest, status)
|
|
values ($username, $passwordDigest, 'unverified')
|
|
returning id as user_id
|
|
), new_team as (
|
|
insert into "team" (display_name)
|
|
values ($teamName)
|
|
returning id as team_id
|
|
)
|
|
insert into "team_user" (user_id, team_id, status)
|
|
values (
|
|
(select user_id from new_user),
|
|
(select team_id from new_team),
|
|
'owner'
|
|
) returning user_id, team_id
|
|
`,
|
|
{ username, passwordDigest, teamName: `${username}'s First Team` },
|
|
),
|
|
);
|
|
if (!result) return null;
|
|
const { userId, teamId } = result;
|
|
return await Promise.all([
|
|
getUser({ id: userId }),
|
|
getTeam({ id: teamId }),
|
|
]);
|
|
}
|
|
|
|
export async function getUser(
|
|
{ id, username }: Partial<User>,
|
|
): Promise<User | null> {
|
|
if (!id && !username) throw "getUser called without id or username";
|
|
const column = id ? "id" : "username";
|
|
return singleRow(
|
|
await queryObject<User>(
|
|
`select * from "user" where "${column}" = $1`,
|
|
[id || username],
|
|
),
|
|
);
|
|
}
|
|
|
|
export async function getTeam(
|
|
{ id }: Partial<Team>,
|
|
): Promise<Team | null> {
|
|
return singleRow(
|
|
await queryObject<Team>(
|
|
`select * from "team" where "id" = $1`,
|
|
[id],
|
|
),
|
|
);
|
|
}
|
|
|
|
function someRows<T>(result: { rows: T[] } | null): T[] | null {
|
|
console.debug(result);
|
|
if (!result || result.rows.length < 1) return null;
|
|
else return result.rows;
|
|
}
|
|
|
|
function singleRow<T>(result: { rows: T[] } | null): T | null {
|
|
if (!result || result.rows.length < 1) return null;
|
|
else if (result.rows.length > 1) {
|
|
console.error(
|
|
"This singleRow result brought back more than 1 row:",
|
|
result,
|
|
);
|
|
return null;
|
|
} else return result.rows[0];
|
|
}
|