ls-deno/db/mod.ts

247 lines
6.2 KiB
TypeScript
Raw Normal View History

2022-09-30 15:14:57 -05:00
import {
Pool,
2022-10-08 00:00:45 -05:00
PoolClient,
2022-09-30 15:14:57 -05:00
PostgresError,
} from "https://deno.land/x/postgres@v0.16.1/mod.ts";
import {
type QueryArguments,
2022-10-07 23:22:35 -05:00
type QueryArrayResult,
2022-09-30 15:14:57 -05:00
type QueryObjectResult,
} from "https://deno.land/x/postgres@v0.16.1/query/query.ts?s=QueryArguments";
2022-10-07 23:22:35 -05:00
import { config } from "@/config.ts";
2022-10-08 02:01:48 -05:00
import * as base64 from "$std/encoding/base64.ts";
2022-10-11 12:20:25 -05:00
import { log } from "@/log.ts";
2022-10-07 23:22:35 -05:00
2022-10-08 00:00:45 -05:00
import {
type Identifiable,
type Note,
2022-10-08 00:24:03 -05:00
type Team,
2022-10-08 00:00:45 -05:00
type Timestamped,
2022-10-08 02:01:48 -05:00
type Token,
type TokenDigest,
2022-10-08 00:00:45 -05:00
type User,
} from "@/types.ts";
2022-09-27 15:49:41 -05:00
2022-10-08 02:01:48 -05:00
import { sha256 } from "https://denopkg.com/chiefbiiko/sha256@v1.0.0/mod.ts";
2022-09-30 15:14:57 -05:00
export { PostgresError };
export { type QueryObjectResult };
2022-09-27 15:49:41 -05:00
2022-10-07 23:22:35 -05:00
const pool = new Pool(config.postgres.url, 3, true);
2022-09-27 15:49:41 -05:00
2022-10-08 00:00:45 -05:00
async function dbOp<T>(op: (connection: PoolClient) => Promise<T>) {
2022-09-27 15:49:41 -05:00
let result = null;
2022-10-08 00:00:45 -05:00
let exception = null;
2022-09-27 15:49:41 -05:00
try {
const connection = await pool.connect();
try {
2022-10-08 00:00:45 -05:00
result = await op(connection);
2022-09-27 15:49:41 -05:00
} catch (err) {
2022-10-11 12:20:25 -05:00
log.error("Error querying database:", { ...err });
2022-10-08 00:00:45 -05:00
exception = err;
2022-09-27 15:49:41 -05:00
} finally {
connection.release();
}
} catch (err) {
2022-10-08 00:00:45 -05:00
exception = err;
2022-10-11 12:20:25 -05:00
log.error("Error connecting to database:", err);
2022-09-27 15:49:41 -05:00
}
2022-10-08 00:00:45 -05:00
if (exception != null) throw exception;
2022-09-27 15:49:41 -05:00
return result;
}
2022-10-07 23:22:35 -05:00
2022-10-08 00:00:45 -05:00
export async function queryObject<T>(
2022-10-07 23:22:35 -05:00
sql: string,
2022-10-08 00:00:45 -05:00
args?: QueryArguments,
): Promise<QueryObjectResult<T> | null> {
2022-10-08 02:53:13 -05:00
return await dbOp(async (connection) => {
const result = await connection.queryObject<T>({
2022-10-08 00:00:45 -05:00
camelcase: true,
text: sql.trim(),
args,
2022-10-08 02:53:13 -05:00
});
2022-10-11 12:20:25 -05:00
log.debug(result);
2022-10-08 02:53:13 -05:00
return result;
});
2022-10-08 00:00:45 -05:00
}
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,
})
);
2022-10-07 23:22:35 -05:00
}
2022-10-08 00:24:03 -05:00
export async function listNotes(): Promise<Note[] | null> {
return someRows(
2022-10-08 02:53:13 -05:00
await queryObject<Note & User>(
'select u.username as user_username, u.display_name as user_display_name, n.* from note n left join "user" u on u.id = n.user_id order by n.created_at desc',
2022-10-08 00:24:03 -05:00
),
2022-10-07 23:22:35 -05:00
);
}
2022-10-08 00:24:03 -05:00
export async function getNote(
id: string | { id: string },
): Promise<Note | null> {
2022-10-07 23:22:35 -05:00
const idVal = typeof id == "object" ? id.id : id;
2022-10-11 12:20:25 -05:00
log.debug("getNote id =", JSON.stringify(idVal));
2022-10-08 00:24:03 -05:00
return singleRow(
await queryObject<Note>(
"select * from note where id = $1",
[idVal],
),
2022-10-07 23:22:35 -05:00
);
}
2022-10-08 00:00:45 -05:00
type Ungenerated<T> = Omit<T, keyof Identifiable | keyof Timestamped>;
2022-10-08 00:24:03 -05:00
export async function createNote(
2022-10-08 02:53:13 -05:00
{ content, userId }: Ungenerated<Note>,
2022-10-08 00:24:03 -05:00
): Promise<Note | null> {
return singleRow(
await queryObject<Note>(
2022-10-08 02:53:13 -05:00
"insert into note (content, user_id) values ($1, $2) returning *",
[content, userId],
2022-10-08 00:24:03 -05:00
),
2022-10-07 23:22:35 -05:00
);
}
2022-10-08 00:00:45 -05:00
export async function createUser(
{ username, passwordDigest }: Ungenerated<User>,
2022-10-08 00:24:03 -05:00
): Promise<[User | null, Team | null] | null> {
const result = singleRow(
await queryObject<{ teamId: string; userId: string }>(
`
2022-10-08 00:00:45 -05:00
with new_user as (
2022-10-08 02:01:48 -05:00
insert into "user" (username, password_digest)
values ($username, $passwordDigest)
2022-10-08 00:00:45 -05:00
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'
2022-10-08 00:24:03 -05:00
) returning user_id, team_id
2022-10-08 00:00:45 -05:00
`,
2022-10-08 00:24:03 -05:00
{ username, passwordDigest, teamName: `${username}'s First Team` },
),
2022-10-08 00:00:45 -05:00
);
2022-10-08 00:24:03 -05:00
if (!result) return null;
const { userId, teamId } = result;
return await Promise.all([
getUser({ id: userId }),
getTeam({ id: teamId }),
]);
2022-10-08 00:00:45 -05:00
}
2022-10-08 02:01:48 -05:00
const TOKEN_SIZE = 32;
export async function createToken(
token: Omit<Ungenerated<Token>, "digest">,
): Promise<Token | null> {
const intermediateToken: Partial<Token> = { ...token };
if (!intermediateToken.bytes) {
intermediateToken.bytes = new Uint8Array(TOKEN_SIZE);
crypto.getRandomValues(intermediateToken.bytes);
}
if (!intermediateToken.digest) {
const digest = sha256(intermediateToken.bytes);
if (!(digest instanceof Uint8Array)) throw "token digest was non-brinary";
intermediateToken.digest = digest;
}
if (!intermediateToken.data) intermediateToken.data = null;
const result = singleRow(
await queryObject<Token>(
`
insert into "user_token" (digest, user_id, data)
values ($digest, $userId, $data)
returning *
`,
intermediateToken,
),
);
if (result) return { ...intermediateToken, ...result };
return null;
}
export async function getToken(token: TokenDigest): Promise<Token | null> {
const digest = base64.decode(token);
return singleRow(
await queryObject(
`
select * from user_token where digest = $1
`,
[digest],
),
);
}
2022-10-08 00:24:03 -05:00
export async function getUser(
{ id, username }: Partial<User>,
): Promise<User | null> {
2022-10-08 00:00:45 -05:00
if (!id && !username) throw "getUser called without id or username";
const column = id ? "id" : "username";
2022-10-08 00:24:03 -05:00
return singleRow(
await queryObject<User>(
`select * from "user" where "${column}" = $1`,
[id || username],
),
2022-10-08 00:00:45 -05:00
);
}
2022-10-08 00:24:03 -05:00
2022-10-08 02:01:48 -05:00
export async function getUserFromNonExpiredLoginToken(
token: TokenDigest,
): Promise<User | null> {
const digest = sha256(base64.decode(token));
return singleRow(
await queryObject<User>(
`
select u.* from "user_token" ut
left join "user" u on u.id = ut.user_id
where ut."digest" = $1
and ut."data"->>'type' = 'login'
and now() < (ut.created_at + '7 days'::interval)
`,
[digest],
),
);
}
2022-10-08 00:24:03 -05:00
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 {
2022-10-11 12:20:25 -05:00
log.debug(result);
2022-10-08 00:24:03 -05:00
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) {
2022-10-11 12:20:25 -05:00
log.error(
2022-10-08 00:24:03 -05:00
"This singleRow result brought back more than 1 row:",
result,
);
return null;
} else return result.rows[0];
}