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 ,
2022-10-21 03:06:37 -05:00
Transaction ,
type TransactionOptions ,
2022-09-30 15:14:57 -05:00
} 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-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-11-09 16:55:27 -06:00
import { type PostgresConfig } from "@/config.ts" ;
2022-10-08 00:00:45 -05:00
import {
2022-10-21 03:06:37 -05:00
type Display ,
2022-10-08 00:00:45 -05:00
type Note ,
2022-10-21 03:06:37 -05:00
type Playlist ,
2022-10-08 00:24:03 -05:00
type Team ,
2022-10-21 03:06:37 -05:00
type TeamUser ,
2022-10-08 02:01:48 -05:00
type Token ,
type TokenDigest ,
2022-10-21 03:06:37 -05:00
type Ungenerated ,
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-11-09 16:55:27 -06:00
let pool : Pool ;
export function initDatabaseConnectionPool ( { url } : PostgresConfig ) {
pool = new Pool ( url , 3 , true ) ;
2022-11-10 10:25:35 -06:00
testDbConnection ( ) ;
}
2022-11-10 11:41:46 -06:00
/ * *
* Checks that a certain SQL predicate fetches a row that indeed exists .
*
* sqlSnippet should assume it comes after a 'select * from' . For example : '"user" where id = 1' .
* /
async function rowExists (
sqlSnippet : string ,
args : unknown [ ] ,
) : Promise < boolean > {
const result = await queryArray < [ boolean ] > (
` select exists(select 1 from ${ sqlSnippet } ); ` ,
args ,
) ;
if ( result && result . rows . length > 0 ) {
return ! ! ( result . rows [ 0 ] [ 0 ] ) ;
}
return false ;
}
export async function isUserInTeam (
userId : string ,
teamId : string ,
) : Promise < boolean > {
return await rowExists ( "team_user where user_id = $1 and team_id = $2" , [
userId ,
teamId ,
] ) ;
}
2022-11-10 10:25:35 -06:00
export async function testDbConnection ( ) : Promise < boolean > {
try {
await dbOp ( ( conn ) = > conn . queryObject ( "select 1" ) ) ;
log . info ( "Successfully connected to database" ) ;
return true ;
} catch ( e ) {
log . critical ( "Failed to connect to database:" , e ) ;
return false ;
}
2022-11-09 16:55:27 -06:00
}
2022-09-27 15:49:41 -05:00
2022-10-21 03:06:37 -05:00
type QueryResult < T > = { rows : T [ ] } | null ;
class NoRowsError < T > extends Error {
result : QueryResult < T > ;
constructor ( result : QueryResult < T > ) {
const message = ` No rows in query result: ${ result } ` ;
super ( message ) ;
this . result = result ;
}
}
class TooManyRowsError < T > extends Error {
result : QueryResult < T > ;
constructor ( result : QueryResult < T > ) {
const message = ` Too many rows in query result: ${ result } ` ;
super ( message ) ;
this . result = result ;
}
}
function someRows < T > ( result : QueryResult < T > ) : T [ ] {
if ( ! result || result . rows . length < 1 ) {
throw new NoRowsError ( result ) ;
} else {
return result . rows ;
}
}
function singleRow < T > ( result : QueryResult < T > ) : T {
if ( ! result || result . rows . length < 1 ) throw new NoRowsError ( result ) ;
else if ( result . rows . length > 1 ) throw new TooManyRowsError ( result ) ;
else return result . rows [ 0 ] ;
}
export async function dbOp < T > (
op : ( connection : PoolClient ) = > Promise < T > ,
) : Promise < T > {
let result : T | null = 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-08 00:00:45 -05:00
exception = err ;
2022-09-27 15:49:41 -05:00
} finally {
connection . release ( ) ;
}
} catch ( err ) {
2022-11-09 16:55:27 -06:00
log . critical ( "Error connecting to database:" , err ) ;
2022-11-10 14:34:27 -06:00
exception = err ;
2022-09-27 15:49:41 -05:00
}
2022-10-08 00:00:45 -05:00
if ( exception != null ) throw exception ;
2022-10-21 03:06:37 -05:00
if ( result == null ) {
throw "Database operation failed to properly load a result" ;
}
2022-09-27 15:49:41 -05:00
return result ;
}
2022-10-07 23:22:35 -05:00
2022-11-10 11:41:46 -06:00
/ * *
* Example : queryObject ( 'select * from "user" where id = $1' , [ userId ] )
* /
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 ,
2022-10-21 03:06:37 -05:00
connection? : PoolClient | Transaction ,
2022-10-08 00:00:45 -05:00
) : Promise < QueryObjectResult < T > | null > {
2022-10-21 03:06:37 -05:00
if ( ! connection ) {
return await dbOp ( async ( connection ) = > {
return await queryObject ( sql , args , connection ) ;
} ) ;
} else {
2022-10-08 02:53:13 -05:00
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
} ) ;
return result ;
2022-10-21 03:06:37 -05:00
}
2022-10-08 00:00:45 -05:00
}
2022-11-10 11:41:46 -06:00
/ * *
* Example : queryArray ( 'select * from "user" where id = $1' , [ userId ] )
* /
export async function queryArray < T extends unknown [ ] > (
2022-10-08 00:00:45 -05:00
sql : string ,
args? : QueryArguments ,
2022-10-21 03:06:37 -05:00
connection? : PoolClient ,
2022-10-08 00:00:45 -05:00
) : Promise < QueryArrayResult < T > | null > {
2022-10-21 03:06:37 -05:00
if ( ! connection ) {
return await dbOp ( async ( connection ) = > {
return await queryArray < T > ( sql , args , connection ) ;
} ) ;
} else {
const result = await connection . queryArray < T > ( {
2022-10-08 00:00:45 -05:00
text : sql.trim ( ) ,
args ,
2022-10-21 03:06:37 -05:00
} ) ;
return result ;
}
2022-10-07 23:22:35 -05:00
}
2022-10-21 03:06:37 -05:00
export async function listNotes ( ) : Promise < ( Note & User ) [ ] > {
2022-10-08 00:24:03 -05:00
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 } ,
2022-10-21 03:06:37 -05:00
) : Promise < Note > {
2022-10-07 23:22:35 -05:00
const idVal = typeof id == "object" ? id.id : id ;
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: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
2022-10-21 03:06:37 -05:00
export async function createTeamUser (
{ teamId , userId , status } : TeamUser ,
transaction? : Transaction ,
) : Promise < TeamUser | null > {
return singleRow (
await queryObject < TeamUser > (
2022-10-08 00:24:03 -05:00
`
2022-10-21 03:06:37 -05:00
insert into "team_user" ( user_id , team_id , status )
values (
$userId ,
$teamId ,
$status
) returning * ` ,
{ userId , teamId , status } ,
transaction ,
2022-10-08 00:24:03 -05:00
) ,
2022-10-08 00:00:45 -05:00
) ;
2022-10-21 03:06:37 -05:00
}
export async function createTeam (
data : {
team : Ungenerated < Team > ;
creator? : User ;
} ,
transaction? : Transaction ,
) : Promise < Team > {
if ( ! transaction ) {
return await wrapWithTransaction < Team > (
"createTeam" ,
( t ) = > createTeam ( data , t ) ,
) ;
} else {
try {
const { team : { displayName } , creator } = data ;
const team = singleRow (
await queryObject < Team > (
` insert into "team" (display_name) values ( $ displayName) returning * ` ,
{ displayName } ,
transaction ,
) ,
) ;
if ( creator ) {
await createTeamUser (
{ teamId : team.id , userId : creator.id , status : "owner" } ,
transaction ,
) ;
}
return team ;
} catch ( e ) {
throw e ;
}
}
}
export async function wrapWithTransaction < T > (
transactionName : string ,
callback : ( transaction : Transaction ) = > Promise < T > ,
transactionOptions? : TransactionOptions ,
) : Promise < T > {
const result = await dbOp < T > ( async ( connection ) = > {
try {
const transaction = connection . createTransaction (
transactionName ,
transactionOptions ,
) ;
try {
await transaction . begin ( ) ;
const result : T = await callback ( transaction ) ;
await transaction . commit ( ) ;
return result ;
} catch ( e ) {
await transaction . rollback ( ) ;
throw e ;
}
} catch ( e ) {
throw e ;
}
} ) ;
if ( ! result ) throw "Failed to finish transactional database operation" ;
return result ;
}
export async function createUser (
data : Ungenerated < User > ,
transaction? : Transaction ,
) : Promise < User > {
if ( ! transaction ) {
return await wrapWithTransaction < User > (
"createUser" ,
( t ) = > createUser ( data , t ) ,
) ;
} else {
try {
const { username , passwordDigest } = data ;
const user = singleRow (
await queryObject < User > (
` insert into "user" (username, password_digest)
values ( $username , $passwordDigest )
returning * ` ,
{ username , passwordDigest } ,
transaction ,
) ,
) ;
await createTeam ( {
2022-11-09 16:55:27 -06:00
team : { displayName : ` ${ username } 's Team ` } ,
2022-10-21 03:06:37 -05:00
creator : user ,
} , transaction ) ;
return user ;
} catch ( e ) {
throw e ;
}
}
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" > ,
2022-10-21 03:06:37 -05:00
) : Promise < Token > {
2022-10-08 02:01:48 -05:00
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 > (
`
2022-10-21 03:06:37 -05:00
insert into "token" ( digest , user_id , data )
2022-10-08 02:01:48 -05:00
values ( $digest , $userId , $data )
returning *
` ,
intermediateToken ,
) ,
) ;
2022-10-21 03:06:37 -05:00
return { . . . intermediateToken , . . . result } ;
2022-10-08 02:01:48 -05:00
}
2022-10-12 03:31:26 -05:00
export async function deleteToken (
token : TokenDigest ,
) {
const digest = sha256 ( base64 . decode ( token ) ) ;
return await queryObject (
2022-10-21 03:06:37 -05:00
` delete from "token" where digest = $ 1 ` ,
2022-10-12 03:31:26 -05:00
[ digest ] ,
) ;
}
2022-10-21 03:06:37 -05:00
export async function getToken ( token : TokenDigest ) : Promise < Token > {
2022-10-12 03:31:26 -05:00
const digest = sha256 ( base64 . decode ( token ) ) ;
2022-10-08 02:01:48 -05:00
return singleRow (
await queryObject (
2022-10-21 03:06:37 -05:00
` select * from "token" where digest = $ 1 ` ,
2022-10-08 02:01:48 -05:00
[ digest ] ,
) ,
) ;
}
2022-10-08 00:24:03 -05:00
export async function getUser (
2022-10-21 03:06:37 -05:00
idOrUsername : { id : string } | { username : string } | string ,
) : Promise < User > {
if ( typeof idOrUsername == "string" ) {
try {
return singleRow (
await queryObject < User > (
` select * from "user" where "id" = $ 1 ` ,
[ idOrUsername ] ,
) ,
) ;
} catch ( _ ) {
return singleRow (
await queryObject < User > (
` select * from "user" where "username" = $ 1 ` ,
[ idOrUsername ] ,
) ,
) ;
}
} else {
const column = "id" in idOrUsername ? "id" : "username" ;
return singleRow (
await queryObject < User > (
` select * from "user" where " ${ column } " = $ 1 ` ,
[ ( idOrUsername as { id? : string ; username? : string } ) [ column ] ] ,
) ,
) ;
}
2022-10-08 00:00:45 -05:00
}
2022-10-08 00:24:03 -05:00
2022-11-10 10:25:35 -06:00
// TODO: refresh token?
2022-10-08 02:01:48 -05:00
export async function getUserFromNonExpiredLoginToken (
token : TokenDigest ,
2022-10-21 03:06:37 -05:00
) : Promise < User > {
2022-10-12 03:31:26 -05:00
// TODO: if the token has expired, return a specific error?
2022-10-08 02:01:48 -05:00
const digest = sha256 ( base64 . decode ( token ) ) ;
return singleRow (
await queryObject < User > (
2022-10-21 03:06:37 -05:00
` select u.* from "token" ut
2022-10-08 02:01:48 -05:00
left join "user" u on u . id = ut . user_id
where ut . "digest" = $1
and ut . "data" - >> 'type' = 'login'
2022-10-21 03:06:37 -05:00
and now ( ) < ( ut . created_at + '14 days' : : interval ) ` ,
2022-10-08 02:01:48 -05:00
[ digest ] ,
) ,
) ;
}
2022-10-08 00:24:03 -05:00
export async function getTeam (
{ id } : Partial < Team > ,
2022-10-21 03:06:37 -05:00
) : Promise < Team > {
2022-10-08 00:24:03 -05:00
return singleRow (
await queryObject < Team > (
` select * from "team" where "id" = $ 1 ` ,
[ id ] ,
) ,
) ;
}
2022-10-21 03:06:37 -05:00
export async function getUserTeams (
{ id } : Partial < User > ,
) : Promise < Team [ ] > {
return someRows (
await queryObject < Team > (
` select t.* from "team" t
left join "team_user" tu on t . id = tu . team_id
where tu . "user_id" = $1 ` ,
[ id ] ,
) ,
) ;
2022-10-08 00:24:03 -05:00
}
2022-10-21 03:06:37 -05:00
export async function getTeamUsers (
{ id } : Partial < Team > ,
) : Promise < User [ ] > {
return someRows (
await queryObject < User > (
` select u.* from "user" u
left join "team_user" tu on u . id = tu . user_id
where tu . "team_id" = $1 ` ,
[ id ] ,
) ,
) ;
2022-10-08 00:24:03 -05:00
}
2022-10-21 03:06:37 -05:00
// export async function createDisplay(display: Ungenerated<Display>, transaction?: Transaction) {
// display
// }