2023-05-11 10:25:51 +02:00
/* global defineEventHandler, getQuery, createError, readBody, setResponseStatus */
2023-05-11 09:11:20 +02:00
import { QueryObject } from "ufo" ;
import { H3Event } from "h3" ;
import { ResultSetHeader } from "mysql2/promise" ;
import { data , database } from "./database" ;
import { isString } from "./isString" ;
2023-05-24 09:40:45 +02:00
import Snowflake from "~/utils/snowflake" ;
2023-05-11 09:11:20 +02:00
import { client } from "~/utils/types/database" ;
2023-05-11 06:03:22 +02:00
type queryType = {
type : "before" | "after" | "around" ,
id : string
} | {
type : null
} ;
/ * *
* Before , around , after pagination wrapper
* /
2023-05-11 09:11:20 +02:00
export default class BaaPagination < T extends { [ k : string ] : any } , keyType extends string = "id" > {
2023-05-11 06:03:22 +02:00
readonly table : string ;
2023-05-11 09:11:20 +02:00
readonly key : keyType ;
2023-05-11 10:51:41 +02:00
readonly select : string ;
2023-05-11 11:09:28 +02:00
readonly groupBy : string ;
private get sqlGroupBy() {
return this . groupBy !== "" ? ` GROUP BY ${ this . groupBy } ` : "" ;
}
2023-05-11 09:11:20 +02:00
/ * *
* Gets queryType for a given query with a value
* @param query the query to parse
* @throws if query malformed ( multiple before / after / around )
* /
static getLocationParameterType ( query : QueryObject ) : queryType {
const before = query . before ;
const after = query . after ;
const around = query . around ;
let setLocationParametersCount = 0 ;
let rvalue : queryType = { type : null } ;
if ( isString ( before ) ) {
setLocationParametersCount ++ ;
rvalue = { type : "before" , id : before } ;
}
if ( isString ( after ) ) {
setLocationParametersCount ++ ;
rvalue = { type : "after" , id : after } ;
}
if ( isString ( around ) ) {
setLocationParametersCount ++ ;
rvalue = { type : "around" , id : around } ;
}
if ( setLocationParametersCount > 1 ) {
throw createError ( {
statusCode : 400 ,
message : "multiple location parameters not allowed" ,
} ) ;
}
return rvalue ;
}
async getPagedResults (
queryType : queryType ,
limit = 50 ,
2023-05-11 10:43:05 +02:00
where = "" ,
bind : Array < any > = [ ] ,
2023-05-11 09:11:20 +02:00
) {
2023-05-11 11:02:08 +02:00
const sqlwhere = where !== "" ? ` AND ( ${ where } ) ` : "" ;
2023-05-11 09:11:20 +02:00
switch ( queryType . type ) {
case "before" : {
const [ data ] = await database . query (
2023-05-11 11:09:28 +02:00
` SELECT ${ this . select } , CONVERT( \` ${ this . key } \` , CHAR) AS \` ${ this . key } \` FROM \` ${ this . table } \` WHERE \` ${ this . key } \` < ? ${ sqlwhere } ORDER BY \` ${ this . key } \` DESC ${ this . sqlGroupBy } LIMIT ? ` ,
2023-05-11 10:43:05 +02:00
[ queryType . id , . . . bind , limit ] ,
2023-05-11 09:11:20 +02:00
) as unknown as data < T > ;
return data ;
}
case "after" : {
const [ data ] = await database . query (
2023-05-11 11:09:28 +02:00
` SELECT ${ this . select } , CONVERT( \` ${ this . key } \` , CHAR) AS \` ${ this . key } \` FROM \` ${ this . table } \` WHERE \` ${ this . key } \` > ? ${ sqlwhere } ORDER BY \` ${ this . key } \` DESC ${ this . sqlGroupBy } LIMIT ? ` ,
2023-05-11 10:43:05 +02:00
[ queryType . id , . . . bind , limit ] ,
2023-05-11 09:11:20 +02:00
) as unknown as data < T > ;
return data ;
}
case "around" : {
const [ data ] = await database . query (
2023-05-11 10:51:41 +02:00
` SELECT ${ this . select } , CONVERT( \` ${ this . key } \` , CHAR) AS \` ${ this . key } \` FROM ( \ n ` +
2023-05-11 11:09:28 +02:00
` (SELECT * FROM \` ${ this . table } \` WHERE \` ${ this . key } \` >= ? ${ sqlwhere } ORDER BY \` ${ this . key } \` ${ this . sqlGroupBy } ASC LIMIT ?) \ n ` +
2023-05-11 09:11:20 +02:00
"UNION ALL\n" +
2023-05-11 11:09:28 +02:00
` (SELECT ${ this . select } FROM \` ${ this . table } \` WHERE \` ${ this . key } \` < ? ${ sqlwhere } ORDER BY \` ${ this . key } \` DESC ${ this . sqlGroupBy } LIMIT ?) \ n ` +
2023-05-11 10:27:24 +02:00
` ) as \` x \` ORDER BY \` ${ this . key } \` DESC ` ,
2023-05-11 10:43:05 +02:00
[ queryType . id , . . . bind , Math . ceil ( limit / 2 ) , queryType . id , . . . bind , Math . floor ( limit / 2 ) ] ,
2023-05-11 09:11:20 +02:00
) as unknown as data < T > ;
return data ;
}
case null : {
const [ data ] = await database . query (
2023-05-11 11:09:28 +02:00
` SELECT ${ this . select } , CONVERT( \` ${ this . key } \` , CHAR) AS \` ${ this . key } \` FROM \` ${ this . table } \` WHERE TRUE ${ sqlwhere } ORDER BY \` ${ this . key } \` DESC ${ this . sqlGroupBy } LIMIT ? ` ,
2023-05-11 10:43:05 +02:00
[ . . . bind , limit ] ,
2023-05-11 09:11:20 +02:00
) as unknown as data < T > ;
return data ;
}
default :
throw createError ( "Not implemented" ) ;
}
}
2023-05-11 10:43:05 +02:00
RESTget (
e : H3Event ,
defaultLimit = 50 ,
limitLimit = 200 ,
where = "" ,
bind : Array < any > = [ ] ,
) {
2023-05-11 09:11:20 +02:00
const query = getQuery ( e ) ;
let limit = defaultLimit ;
if ( query . limit ) limit = Number ( query . limit ) ;
if ( limit > limitLimit ) {
throw createError ( {
statusCode : 400 ,
message : ` Cannot retrieve more than ${ limitLimit } records ` ,
} ) ;
}
if ( limit <= 0 ) {
throw createError ( {
statusCode : 400 ,
message : "Tried to retireve 0 or less records" ,
} ) ;
}
const queryData = BaaPagination . getLocationParameterType ( query ) ;
2023-05-11 10:43:05 +02:00
return this . getPagedResults ( queryData , limit , where , bind ) ;
2023-05-11 09:11:20 +02:00
}
async RESTpost < K extends keyof Omit < T , keyType > > (
e : H3Event ,
fields : Array < K > ,
valueChecker : ( obj : unknown ) = > obj is { [ P in K ] : T [ P ] } ,
) {
const body = await readBody ( e ) ;
const id = new Snowflake ( ) . toString ( ) ;
if ( ! valueChecker ( body ) ) throw createError ( { message : "Invalid body" , statusCode : 400 } ) ;
const arrayToInsert : Array < any > = [ id ] ;
arrayToInsert . push ( . . . fields . map ( field = > body [ field ] ) ) ;
await database . query (
2023-05-11 10:49:47 +02:00
` INSERT INTO \` ${ this . table } \` ` +
2023-05-11 09:11:20 +02:00
` ( \` ${ this . key } \` , \` ${ fields . join ( "`, `" ) } \` ) ` +
"VALUES (" +
"?, " . repeat ( fields . length ) +
"?)" ,
arrayToInsert ,
) ;
2023-05-11 10:25:51 +02:00
setResponseStatus ( e , 201 ) ;
2023-05-11 09:11:20 +02:00
// FIXME: data may be turncated in the database
// either throw an error when data is too large or
// reply with turncated data
return { id , . . . body } ;
}
async RESTgetRecord ( e : H3Event ) {
const key = e . context . params ? . [ this . key ] ;
const [ data ] = await database . query (
2023-05-11 10:51:41 +02:00
` SELECT ${ this . select } , CONVERT( \` ${ this . key } \` , CHAR) AS \` ${ this . key } \` FROM \` ${ this . table } \` WHERE \` ${ this . key } \` = ? ` ,
2023-05-11 09:11:20 +02:00
[ key ] ,
) as data < T > ;
if ( ! data [ 0 ] ) {
throw createError ( {
statusCode : 404 ,
} ) ;
}
return data [ 0 ] ;
}
async RESTpatchRecord (
e : H3Event ,
valueChecker : ( obj : unknown ) = > obj is Partial < Omit < T , keyType > > ,
) {
const body = await readBody ( e ) ;
const key = e . context . params ? . [ this . key ] ;
if ( ! valueChecker ( body ) ) throw createError ( { message : "Invalid body" , statusCode : 400 } ) ;
for ( const [ k , v ] of Object . entries ( body ) ) {
// FIXME: use single database.query method instead of looping through keys and values
const [ res ] = await database . query (
// I believe it is safe to put key (k) in the template
// because it is limited to 4 values here
` UPDATE \` ${ this . table } \` SET \` ${ k } \` = ? WHERE \` ${ this . key } \` = ? ` ,
[ v , key ] ,
) as unknown as [ ResultSetHeader ] ;
if ( res . affectedRows !== 1 ) {
throw createError ( {
statusCode : 404 ,
} ) ;
}
}
const [ data ] = await database . query (
2023-05-11 10:51:41 +02:00
` SELECT ${ this . select } , CONVERT( \` ${ this . key } \` , CHAR) AS \` ${ this . key } \` FROM \` ${ this . table } \` WHERE \` ${ this . key } \` = ? ` ,
2023-05-11 09:11:20 +02:00
[ key ] ,
) as data < T > ;
return data [ 0 ] ;
}
async RESTdeleteRecord ( e : H3Event ) {
const key = e . context . params ? . [ this . key ] ;
const [ result ] = await database . query (
` DELETE FROM \` ${ this . table } \` WHERE \` ${ this . key } \` = ? ` ,
[ key ] ,
) as unknown as [ ResultSetHeader ] ;
if ( result . affectedRows === 0 ) throw createError ( { statusCode : 404 } ) ;
return null ;
}
2023-05-11 10:43:05 +02:00
async RESTrecordCount (
e :H3Event ,
where = "" ,
bind : Array < any > = [ ] ,
) {
2023-05-11 11:02:08 +02:00
const sqlwhere = where !== "" ? ` WHERE ${ where } ` : "" ;
2023-05-11 09:11:20 +02:00
const [ [ data ] ] = await database . query (
2023-05-11 11:09:28 +02:00
` SELECT COUNT(*) as \` count \` FROM \` ${ this . table } \` ${ sqlwhere } ${ this . sqlGroupBy } ` ,
2023-05-11 10:43:05 +02:00
bind ,
2023-05-11 09:11:20 +02:00
) as data < { count : number } > ;
if ( ! data ) throw createError ( "Database returned no rows" ) ;
return data ;
}
2023-05-11 06:03:22 +02:00
2023-05-11 11:09:28 +02:00
constructor (
table : string ,
key : keyType ,
select = "*" ,
groupBy = "" ,
) {
2023-05-11 06:03:22 +02:00
this . table = table ;
this . key = key ;
2023-05-11 10:51:41 +02:00
this . select = select ;
2023-05-11 11:09:28 +02:00
this . groupBy = groupBy ;
2023-05-11 06:03:22 +02:00
}
}