Better db functions

This commit is contained in:
Daniel Flanagan 2022-10-08 00:24:03 -05:00
parent f592524be1
commit 967919f9e7
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
6 changed files with 99 additions and 47 deletions

View file

@ -152,12 +152,17 @@ try {
} }
try { try {
await createNote({ content: "Hello, notes!" }); console.debug(
await createUser({ await Promise.all([
username: "lytedev", createNote({ content: "Hello, notes!" }),
passwordDigest: createUser({
"$2a$10$9fyDAOz6H4a393KHyjbvIe1WFxbhCJhq/CZmlXcEg4d1bE9Ey25WW", username: "lytedev",
}); passwordDigest:
"$2a$10$9fyDAOz6H4a393KHyjbvIe1WFxbhCJhq/CZmlXcEg4d1bE9Ey25WW",
status: "superadmin",
}),
]),
);
} catch (err) { } catch (err) {
console.log("Failed to run seed database:", { ...err }); console.log("Failed to run seed database:", { ...err });
throw err; throw err;

View file

@ -13,6 +13,7 @@ import { config } from "@/config.ts";
import { import {
type Identifiable, type Identifiable,
type Note, type Note,
type Team,
type Timestamped, type Timestamped,
type User, type User,
} from "@/types.ts"; } from "@/types.ts";
@ -68,35 +69,46 @@ export async function queryArray<T extends []>(
); );
} }
export async function listNotes() { export async function listNotes(): Promise<Note[] | null> {
return await queryObject<Note>( return someRows(
"select * from note order by created_at desc", await queryObject<Note>(
"select * from note order by created_at desc",
),
); );
} }
export async function getNote(id: string | { id: string }) { export async function getNote(
id: string | { id: string },
): Promise<Note | null> {
const idVal = typeof id == "object" ? id.id : id; const idVal = typeof id == "object" ? id.id : id;
console.debug("getNote id =", JSON.stringify(idVal)); console.debug("getNote id =", JSON.stringify(idVal));
return await queryObject<Note>( return singleRow(
"select * from note where id = $1", await queryObject<Note>(
[idVal], "select * from note where id = $1",
[idVal],
),
); );
} }
type Ungenerated<T> = Omit<T, keyof Identifiable | keyof Timestamped>; type Ungenerated<T> = Omit<T, keyof Identifiable | keyof Timestamped>;
export async function createNote({ content }: Ungenerated<Note>) { export async function createNote(
return await queryObject<Note>( { content }: Ungenerated<Note>,
"insert into note (content) values ($1) returning *", ): Promise<Note | null> {
[content], return singleRow(
await queryObject<Note>(
"insert into note (content) values ($1) returning *",
[content],
),
); );
} }
export async function createUser( export async function createUser(
{ username, passwordDigest }: Ungenerated<User>, { username, passwordDigest }: Ungenerated<User>,
) { ): Promise<[User | null, Team | null] | null> {
return await queryObject<{ user_id: string }>( const result = singleRow(
` await queryObject<{ teamId: string; userId: string }>(
`
with new_user as ( with new_user as (
insert into "user" (username, password_digest, status) insert into "user" (username, password_digest, status)
values ($username, $passwordDigest, 'unverified') values ($username, $passwordDigest, 'unverified')
@ -111,17 +123,56 @@ export async function createUser(
(select user_id from new_user), (select user_id from new_user),
(select team_id from new_team), (select team_id from new_team),
'owner' 'owner'
) returning user_id ) returning user_id, team_id
`, `,
{ username, passwordDigest, teamName: `${username}'s First Team` }, { 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 getUser({ id, username }: Partial<User>) { export async function getTeam(
if (!id && !username) throw "getUser called without id or username"; { id }: Partial<Team>,
const column = id ? "id" : "username"; ): Promise<Team | null> {
return await queryObject<User>( return singleRow(
`select * from "user" where "${column}" = $1`, await queryObject<Team>(
[id || username], `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];
}

View file

@ -5,9 +5,7 @@ import { type Note } from "@/types.ts";
export const handler: Handlers<Note[]> = { export const handler: Handlers<Note[]> = {
async GET(_request, context) { async GET(_request, context) {
const result = await listNotes(); return await context.render(await listNotes() || []);
if (!result) throw "unable to fetch from database";
return await context.render(result.rows);
}, },
}; };

View file

@ -6,9 +6,8 @@ import { type Note } from "@/types.ts";
export const handler: Handlers<Note> = { export const handler: Handlers<Note> = {
async GET(request, context) { async GET(request, context) {
console.debug({ request, context }); console.debug({ request, context });
const result = await getNote(context.params.id); const note = await getNote(context.params.id);
if (!result) throw "unable to fetch from database"; if (!note) throw "unable to fetch from database";
const [note] = result.rows as Note[];
return await context.render(note); return await context.render(note);
}, },
}; };

View file

@ -1,29 +1,24 @@
import { Handlers, PageProps } from "$fresh/server.ts"; import { Handlers, PageProps } from "$fresh/server.ts";
import { queryObject } from "@/db/mod.ts"; import { createNote } from "@/db/mod.ts";
import { Page } from "@/components/Page.tsx"; import { Page } from "@/components/Page.tsx";
import { type Note } from "@/types.ts";
type NoteID = string; export const handler: Handlers<Note> = {
export const handler: Handlers<NoteID> = {
async POST(request, context) { async POST(request, context) {
const content = (await request.formData()).get("content"); const content = (await request.formData()).get("content");
if (!content) throw "no content provided"; if (!content) throw "no content provided";
const result = await queryObject<{ id: string }>( const note = await createNote({ content: content.toString() });
"insert into note (content) values ($1) returning id", if (!note) throw "no note created";
[content], return await context.render(note);
);
if (!result) throw "insert failed";
const { rows: [{ id }] } = result;
return await context.render(id);
}, },
}; };
export default function NotesPage({ data: noteId }: PageProps<NoteID>) { export default function NotesPage({ data: { id } }: PageProps<Note>) {
return ( return (
<Page> <Page>
<h1>You created a note!</h1> <h1>You created a note!</h1>
<a href="/note">Back to notes</a> <a href="/note">Back to notes</a>
<a href={`/note/${noteId}`}>View your note</a> <a href={`/note/${id}`}>View your note</a>
</Page> </Page>
); );
} }

View file

@ -16,6 +16,10 @@ export interface Note extends Identifiable, Timestamped {
content: string; content: string;
} }
export interface Team extends Identifiable, Timestamped {
displayName: string;
}
export type UserStatus = "unverified" | "verified" | "superadmin"; export type UserStatus = "unverified" | "verified" | "superadmin";
export interface User extends Identifiable, Timestamped { export interface User extends Identifiable, Timestamped {