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 ;
}
2022-10-12 03:31:26 -05:00
log . debug (
` intermediateToken bytes: ${ base64 . encode ( intermediateToken . bytes ) } ` ,
) ;
log . debug (
` intermediateToken digest: ${ base64 . encode ( intermediateToken . digest ) } ` ,
) ;
2022-10-08 02:01:48 -05:00
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 ;
}
2022-10-12 03:31:26 -05:00
export async function deleteToken (
token : TokenDigest ,
) {
const digest = sha256 ( base64 . decode ( token ) ) ;
return await queryObject (
`
delete from user_token where digest = $1
` ,
[ digest ] ,
) ;
}
2022-10-08 02:01:48 -05:00
export async function getToken ( token : TokenDigest ) : Promise < Token | null > {
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 (
`
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 > {
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 > (
`
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'
2022-10-12 03:31:26 -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 > ,
) : 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 ] ;
}