diff --git a/server/api/clients.get.ts b/server/api/clients.get.ts index ec25e51..42e6cca 100644 --- a/server/api/clients.get.ts +++ b/server/api/clients.get.ts @@ -1,112 +1,10 @@ /* global defineEventHandler getQuery, createError */ -import { QueryObject } from "ufo"; -import { data, database } from "../utils/database"; -import { isString } from "../utils/isString"; +import BaaPagination from "~/server/utils/baaPagination"; import { client } from "~/utils/types/database"; -type queryType = { - type: "before" | "after" | "around", - id: string -} | { - type: null -}; +export const baaWrapper = new BaaPagination("clients", "id"); -function 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 function getResults( - queryType: queryType, - limit = 50, -) { - switch (queryType.type) { - case "before": { - const [data] = await database.query( - "SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` WHERE `id` < ? ORDER BY `id` DESC LIMIT ?", - [queryType.id, limit], - ) as unknown as data; - return data; - } - case "after": { - const [data] = await database.query( - "SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` WHERE `id` > ? ORDER BY `id` DESC LIMIT ?", - [queryType.id, limit], - ) as unknown as data; - return data; - } - case "around": { - const [data] = await database.query( - "(SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` WHERE `id` >= ? ORDER BY `id` ASC LIMIT ?)\n" + - "UNION ALL\n" + - "(SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` WHERE `id` < ? ORDER BY `id` DESC LIMIT ?)\n" + - "ORDER BY `id` DESC", - [queryType.id, Math.ceil(limit / 2), queryType.id, Math.floor(limit / 2)], - ) as unknown as data; - return data; - } - case null: { - const [data] = await database.query( - "SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` ORDER BY `id` DESC LIMIT ?", - [limit], - ) as unknown as data; - return data; - } - default: - throw createError("Not implemented"); - } -} - -export default defineEventHandler(async (e) => { - const query = getQuery(e); - - let limit = 50; - if (query.limit) limit = Number(query.limit); - if (limit > 200) { - throw createError({ - statusCode: 400, - message: "Cannot retrieve more than 200 records", - }); - } - if (limit <= 0) { - throw createError({ - statusCode: 400, - message: "Tried to retireve 0 or less records", - }); - } - - const queryData = getLocationParameterType(query); - - const result = await getResults(queryData, limit); - - return result; +export default defineEventHandler((e) => { + return baaWrapper.RESTget(e); }); diff --git a/server/api/clients.post.ts b/server/api/clients.post.ts index 7df3971..84fa7e4 100644 --- a/server/api/clients.post.ts +++ b/server/api/clients.post.ts @@ -1,10 +1,9 @@ /* global defineEventHandler, createError, readBody */ -import { database } from "../utils/database"; -import Snowflake from "../utils/snowflake"; +import { baaWrapper } from "./clients.get"; import { client } from "~/utils/types/database"; -const clientKeys = [ +const clientKeys: Array = [ "name", "address", "phone", @@ -13,7 +12,7 @@ const clientKeys = [ export function checkIsClient( value: any, - required = true, + required = false, ): value is Partial> { const errors = new Map(); @@ -29,7 +28,7 @@ export function checkIsClient( if (!(typeof value.phone === "string" || value.phone === null || (!required && value.phone === undefined))) errors.set("phone", "is not string or null"); if (!(typeof value.email === "string" || value.email === null || (!required && value.email === undefined))) errors.set("email", "is not string or null"); - for (const i in value) + for (const i in value as Partial>) if (!clientKeys.includes(i)) errors.set(i, `excessive property`); if (errors.size !== 0) { @@ -49,19 +48,6 @@ export function checkIsClient( return true; } -export default defineEventHandler(async (e) => { - const body = await readBody(e); - const id = new Snowflake().toString(); - - if (!checkIsClient(body)) return; // checkIsClient already throws an detailed error - - await database.query( - "INSERT INTO `clients` VALUES (?, ?, ?, ?, ?)", - [id, body.name, body.address, body.phone, body.email], - ); - - // 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 }; +export default defineEventHandler((e) => { + return baaWrapper.RESTpost(e, clientKeys as Array>, (o): o is Omit => checkIsClient(o, true)); }); diff --git a/server/api/clients/[id].delete.ts b/server/api/clients/[id].delete.ts index ebed607..a4c2e51 100644 --- a/server/api/clients/[id].delete.ts +++ b/server/api/clients/[id].delete.ts @@ -1,17 +1,7 @@ -/* global defineEventHandler, createError */ -import { ResultSetHeader } from "mysql2"; +/* global defineEventHandler */ -import { database } from "~/server/utils/database"; +import { baaWrapper } from "../clients.get"; -export default defineEventHandler(async (e) => { - const id = e.context.params?.id as string; - - const [result] = await database.query( - "DELETE FROM `clients` WHERE `id` = ?", - [id], - ) as unknown as [ResultSetHeader]; - - if (result.affectedRows === 0) throw createError({ statusCode: 404 }); - - return null; +export default defineEventHandler((e) => { + return baaWrapper.RESTdeleteRecord(e); }); diff --git a/server/api/clients/[id].get.ts b/server/api/clients/[id].get.ts index 0c360a1..2647b77 100644 --- a/server/api/clients/[id].get.ts +++ b/server/api/clients/[id].get.ts @@ -1,20 +1,7 @@ -/* global defineEventHandler, createError */ +/* global defineEventHandler */ -import { database, data } from "~/server/utils/database"; -import { client } from "~/utils/types/database"; +import { baaWrapper } from "../clients.get"; -export default defineEventHandler(async (e) => { - const id = e.context.params?.id; - const [data] = await database.query( - "SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` WHERE `id` = ?", - [id], - ) as unknown as data; - - if (!data[0]) { - throw createError({ - statusCode: 404, - }); - } - - return data[0]; +export default defineEventHandler((e) => { + return baaWrapper.RESTgetRecord(e); }); diff --git a/server/api/clients/[id].patch.ts b/server/api/clients/[id].patch.ts index 6ce30a2..38ff3f8 100644 --- a/server/api/clients/[id].patch.ts +++ b/server/api/clients/[id].patch.ts @@ -1,36 +1,8 @@ -/* global defineEventHandler, readBody, createError */ - -import { ResultSetHeader } from "mysql2"; +/* global defineEventHandler */ import { checkIsClient } from "../clients.post"; -import { client } from "~/utils/types/database"; -import { database, data } from "~/server/utils/database"; +import { baaWrapper } from "../clients.get"; -export default defineEventHandler(async (e) => { - const body = await readBody(e); - const id = e.context.params?.id as string; - - if (!checkIsClient(body, false)) return; // checkIsClient already throws an detailed error - - for (const [k, v] of Object.entries(body)) { - const [res] = await database.query( - // I believe it is safe to put key in the template - // because it is limited to 4 values here - `UPDATE \`clients\` SET \`${k}\` = ? WHERE \`id\` = ?`, - [v, id], - ) as unknown as [ResultSetHeader]; - - if (res.affectedRows !== 1) { - throw createError({ - statusCode: 404, - }); - } - } - - const [data] = await database.query( - "SELECT *, CONVERT(`id`, CHAR) AS `id` FROM `clients` WHERE `id` = ?", - [id], - ) as unknown as data; - - return data[0]; +export default defineEventHandler((e) => { + return baaWrapper.RESTpatchRecord(e, checkIsClient); }); diff --git a/server/api/clients/count.get.ts b/server/api/clients/count.get.ts index 1d2ca13..38b82b5 100644 --- a/server/api/clients/count.get.ts +++ b/server/api/clients/count.get.ts @@ -1,12 +1,7 @@ -/* global defineEventHandler, createError */ +/* global defineEventHandler */ -import { database, data } from "~/server/utils/database"; +import { baaWrapper } from "../clients.get"; -export default defineEventHandler(async (e) => { - const [[data]] = await database.query( - "SELECT COUNT(*) as `count` FROM `clients`", - ) as unknown as data<{count: number}>; - - if (!data) throw createError("Database returned no rows"); - return data; +export default defineEventHandler((e) => { + return baaWrapper.RESTrecordCount(e); }); diff --git a/server/utils/baaPagination.ts b/server/utils/baaPagination.ts index df91047..6920cc4 100644 --- a/server/utils/baaPagination.ts +++ b/server/utils/baaPagination.ts @@ -1,3 +1,12 @@ +/* global defineEventHandler, getQuery, createError, readBody */ +import { QueryObject } from "ufo"; +import { H3Event } from "h3"; +import { ResultSetHeader } from "mysql2/promise"; + +import { data, database } from "./database"; +import { isString } from "./isString"; +import Snowflake from "./snowflake"; +import { client } from "~/utils/types/database"; type queryType = { type: "before" | "after" | "around", @@ -9,11 +18,212 @@ type queryType = { /** * Before, around, after pagination wrapper */ -export default class baaPagination { +export default class BaaPagination { readonly table: string; - readonly key: string; + readonly key: keyType; - constructor(table: string, key: string) { + /** + * 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, + ) { + switch (queryType.type) { + case "before": { + const [data] = await database.query( + `SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` < ? ORDER BY \`${this.key}\` DESC LIMIT ?`, + [queryType.id, limit], + ) as unknown as data; + return data; + } + case "after": { + const [data] = await database.query( + `SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`$this.key\` > ? ORDER BY \`${this.key}\` DESC LIMIT ?`, + [queryType.id, limit], + ) as unknown as data; + return data; + } + case "around": { + const [data] = await database.query( + `(SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` >= ? ORDER BY \`${this.key}\` ASC LIMIT ?)\n` + + "UNION ALL\n" + + `(SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` < ? ORDER BY \`${this.key}\` DESC LIMIT ?)\n` + + "ORDER BY `id` DESC", + [queryType.id, Math.ceil(limit / 2), queryType.id, Math.floor(limit / 2)], + ) as unknown as data; + return data; + } + case null: { + const [data] = await database.query( + `SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` ORDER BY \`${this.key}\` DESC LIMIT ?`, + [limit], + ) as unknown as data; + return data; + } + default: + throw createError("Not implemented"); + } + } + + RESTget(e: H3Event, defaultLimit = 50, limitLimit = 200) { + 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); + + return this.getPagedResults(queryData, limit); + } + + async RESTpost>( + e: H3Event, + fields: Array, + 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 = [id]; + arrayToInsert.push(...fields.map(field => body[field])); + + await database.query( + "INSERT INTO `clients` " + + `(\`${this.key}\`,\`${fields.join("`, `")}\`) ` + + "VALUES (" + + "?, ".repeat(fields.length) + + "?)", + arrayToInsert, + ); + + // 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( + `SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` = ?`, + [key], + ) as data; + + if (!data[0]) { + throw createError({ + statusCode: 404, + }); + } + + return data[0]; + } + + async RESTpatchRecord( + e: H3Event, + valueChecker: (obj: unknown) => obj is Partial>, + ) { + 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( + `SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` = ?`, + [key], + ) as data; + + 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; + } + + async RESTrecordCount(e :H3Event) { + const [[data]] = await database.query( + `SELECT COUNT(*) as \`count\` FROM \`${this.table}\``, + ) as data<{count: number}>; + + if (!data) throw createError("Database returned no rows"); + return data; + } + + constructor(table: string, key: keyType) { this.table = table; this.key = key; }