Replace mysql2 with prisma
also I updated packages, and properly typed api input a lot of time was spent, I don't remeber what really I did x3 but everything was related to replacing mysql2 with prisma
This commit is contained in:
parent
be1e3909b6
commit
eebf25198d
39 changed files with 1081 additions and 1292 deletions
108
server/utils/baaPageParsing.ts
Normal file
108
server/utils/baaPageParsing.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
// BAA stands for Before Around After
|
||||
|
||||
import { getQuery, type H3Event } from "h3";
|
||||
import { type QueryObject } from "ufo";
|
||||
|
||||
import { isString } from "./isString";
|
||||
|
||||
import { createError } from "#imports";
|
||||
|
||||
type queryType<none extends boolean = boolean> = none extends false ? {
|
||||
type: "before" | "after" | "around",
|
||||
id: bigint
|
||||
} : {
|
||||
type: null
|
||||
};
|
||||
|
||||
export type pageData<none extends boolean = boolean> = queryType<none> & { count: number }
|
||||
|
||||
/**
|
||||
* Gets queryType for a given query with a value
|
||||
* @param query the query to parse
|
||||
* @throws if query malformed (multiple before/after/around)
|
||||
*/
|
||||
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: BigInt(before) };
|
||||
}
|
||||
|
||||
if (isString(after)) {
|
||||
setLocationParametersCount++;
|
||||
rvalue = { type: "after", id: BigInt(after) };
|
||||
}
|
||||
|
||||
if (isString(around)) {
|
||||
setLocationParametersCount++;
|
||||
rvalue = { type: "around", id: BigInt(around) };
|
||||
}
|
||||
|
||||
if (setLocationParametersCount > 1) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: "multiple location parameters not allowed",
|
||||
});
|
||||
}
|
||||
|
||||
return rvalue;
|
||||
}
|
||||
|
||||
/** Gets the count parameter from the query object.
|
||||
* @param query the query to check.
|
||||
* @param defaultCount the default count if the query doesn't have count parameter. (default 50)
|
||||
* @param countLimit the maximum count of the parameter before throwing. (default 200)
|
||||
* @returns the value of count parameter.
|
||||
* @throws if the parameter in query exceeds provided countLimit.
|
||||
*/
|
||||
function getRequestedCount(
|
||||
query: QueryObject,
|
||||
defaultCount = 50,
|
||||
countLimit = 200,
|
||||
) {
|
||||
let count = defaultCount;
|
||||
if (query.limit) count = Number(query.limit);
|
||||
|
||||
if (count > countLimit) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: `Cannot retrieve more than ${countLimit} records`,
|
||||
});
|
||||
}
|
||||
if (count <= 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: "Tried to retireve 0 or less records",
|
||||
});
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Gets the baa page parameters from the H3event,
|
||||
* @param e the H3event to fetch parameters.
|
||||
* @param defaultCount the default count to use if there is no count parameter. (default 50)
|
||||
* @param countLimit the maximum value of the count parameter before throwing an error. (default 200)
|
||||
* @returns the page data found in the query.
|
||||
* @throws if event has a count parameter in the query that exceed provided countLimit.
|
||||
*/
|
||||
export default function getPaginatedParameters(
|
||||
e: H3Event,
|
||||
defaultCount = 50,
|
||||
countLimit = 200,
|
||||
): pageData {
|
||||
const query = getQuery(e);
|
||||
const queryParameters = getLocationParameterType(query);
|
||||
const queryCount = getRequestedCount(query, defaultCount, countLimit);
|
||||
|
||||
return {
|
||||
...queryParameters,
|
||||
count: queryCount,
|
||||
};
|
||||
}
|
|
@ -1,261 +0,0 @@
|
|||
import { defineEventHandler, getQuery, readBody, setResponseStatus, H3Event } from "h3";
|
||||
import { type ResultSetHeader } from "mysql2/promise";
|
||||
|
||||
import { type data, database } from "./database";
|
||||
import { isString } from "./isString";
|
||||
import Snowflake from "~/utils/snowflake";
|
||||
import { type client } from "~/utils/types/database";
|
||||
|
||||
import { createError } from "#imports";
|
||||
|
||||
type queryType = {
|
||||
type: "before" | "after" | "around",
|
||||
id: string
|
||||
} | {
|
||||
type: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Before, around, after pagination wrapper
|
||||
*/
|
||||
export default class BaaPagination<T extends {[k: string]: any}, keyType extends string = "id"> {
|
||||
readonly table: string;
|
||||
readonly key: keyType;
|
||||
readonly select: string;
|
||||
readonly groupBy: string;
|
||||
|
||||
private get sqlGroupBy() {
|
||||
return this.groupBy !== "" ? `GROUP BY ${this.groupBy}` : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: any): 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,
|
||||
where = "",
|
||||
bind: Array<any> = [],
|
||||
) {
|
||||
const sqlwhere = where !== "" ? `AND (${where})` : "";
|
||||
switch (queryType.type) {
|
||||
case "before": {
|
||||
const [data] = await database.query(
|
||||
`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 ?`,
|
||||
[queryType.id, ...bind, limit],
|
||||
) as unknown as data<T>;
|
||||
return data;
|
||||
}
|
||||
case "after": {
|
||||
const [data] = await database.query(
|
||||
`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 ?`,
|
||||
[queryType.id, ...bind, limit],
|
||||
) as unknown as data<T>;
|
||||
return data;
|
||||
}
|
||||
case "around": {
|
||||
const [data] = await database.query(
|
||||
` SELECT ${this.select}, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM (\n` +
|
||||
`(SELECT * FROM \`${this.table}\` WHERE \`${this.key}\` >= ? ${sqlwhere} ORDER BY \`${this.key}\` ${this.sqlGroupBy} ASC LIMIT ?)\n` +
|
||||
"UNION ALL\n" +
|
||||
`(SELECT ${this.select} FROM \`${this.table}\` WHERE \`${this.key}\` < ? ${sqlwhere} ORDER BY \`${this.key}\` DESC ${this.sqlGroupBy} LIMIT ?)\n` +
|
||||
`) as \`x\` ORDER BY \`${this.key}\` DESC`,
|
||||
[queryType.id, ...bind, Math.ceil(limit / 2), queryType.id, ...bind, Math.floor(limit / 2)],
|
||||
) as unknown as data<T>;
|
||||
return data;
|
||||
}
|
||||
case null: {
|
||||
const [data] = await database.query(
|
||||
`SELECT ${this.select}, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE TRUE ${sqlwhere} ORDER BY \`${this.key}\` DESC ${this.sqlGroupBy} LIMIT ?`,
|
||||
[...bind, limit],
|
||||
) as unknown as data<T>;
|
||||
return data;
|
||||
}
|
||||
default:
|
||||
throw createError("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
RESTget(
|
||||
e: H3Event,
|
||||
defaultLimit = 50,
|
||||
limitLimit = 200,
|
||||
where = "",
|
||||
bind: Array<any> = [],
|
||||
) {
|
||||
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, where, bind);
|
||||
}
|
||||
|
||||
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(
|
||||
`INSERT INTO \`${this.table}\` ` +
|
||||
`(\`${this.key}\`,\`${fields.join("`, `")}\`) ` +
|
||||
"VALUES (" +
|
||||
"?, ".repeat(fields.length) +
|
||||
"?)",
|
||||
arrayToInsert,
|
||||
);
|
||||
|
||||
setResponseStatus(e, 201);
|
||||
|
||||
// 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 ${this.select}, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` = ?`,
|
||||
[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(
|
||||
`SELECT ${this.select}, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` = ?`,
|
||||
[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;
|
||||
}
|
||||
|
||||
async RESTrecordCount(
|
||||
e :H3Event,
|
||||
where = "",
|
||||
bind: Array<any> = [],
|
||||
) {
|
||||
const sqlwhere = where !== "" ? `WHERE ${where}` : "";
|
||||
const [[data]] = await database.query(
|
||||
`SELECT COUNT(*) as \`count\` FROM \`${this.table}\` ${sqlwhere} ${this.sqlGroupBy}`,
|
||||
bind,
|
||||
) as data<{count: number}>;
|
||||
|
||||
if (!data) throw createError("Database returned no rows");
|
||||
return data;
|
||||
}
|
||||
|
||||
constructor(
|
||||
table: string,
|
||||
key: keyType,
|
||||
select = "*",
|
||||
groupBy = "",
|
||||
) {
|
||||
this.table = table;
|
||||
this.key = key;
|
||||
this.select = select;
|
||||
this.groupBy = groupBy;
|
||||
}
|
||||
}
|
|
@ -1,19 +1,99 @@
|
|||
import mysql, { type Connection } from "mysql2/promise";
|
||||
import { PrismaClient, Prisma } from "@prisma/client";
|
||||
|
||||
const connectionOptions: mysql.ConnectionOptions = {
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT),
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_SCHEMA,
|
||||
decimalNumbers: true,
|
||||
supportBigNumbers: true,
|
||||
};
|
||||
import { type pageData } from "./baaPageParsing";
|
||||
|
||||
export const database =
|
||||
await mysql.createConnection(connectionOptions) as Connection & {
|
||||
new: (localConnectionOptions?: mysql.ConnectionOptions | undefined) => Promise<Connection>
|
||||
};
|
||||
database.new = (localConnectionOptions?: mysql.ConnectionOptions | undefined) => { return mysql.createConnection({ ...localConnectionOptions, ...connectionOptions }); };
|
||||
type model = PrismaClient[Uncapitalize<Prisma.ModelName>];
|
||||
|
||||
export type data<T> = [T[], mysql.FieldPacket[]];
|
||||
function getBeforeParameters<T, A>(
|
||||
pageData: pageData<false>,
|
||||
fetchArgs: Prisma.Args<T, "findMany">,
|
||||
) {
|
||||
const _fetchArgs = Object.assign({}, fetchArgs);
|
||||
return Object.assign(_fetchArgs, {
|
||||
take: pageData.count,
|
||||
orderBy: [
|
||||
{ id: "desc" },
|
||||
],
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
id: {
|
||||
_lt: pageData.id,
|
||||
},
|
||||
},
|
||||
fetchArgs.where,
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getAfterParameters<T>(
|
||||
pageData: pageData<false>,
|
||||
fetchArgs: Prisma.Args<T, "findMany">,
|
||||
) {
|
||||
const _fetchArgs = Object.assign({}, fetchArgs);
|
||||
return Object.assign(_fetchArgs, {
|
||||
take: pageData.count,
|
||||
orderBy: [
|
||||
{ id: "desc" },
|
||||
],
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
id: {
|
||||
_gt: pageData.id,
|
||||
},
|
||||
},
|
||||
fetchArgs.where,
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getNullParameters<T>(
|
||||
pageData: pageData<true>,
|
||||
fetchArgs: Prisma.Args<T, "findMany">,
|
||||
) {
|
||||
const _fetchArgs = Object.assign({}, fetchArgs);
|
||||
return Object.assign(_fetchArgs, {
|
||||
take: pageData.count,
|
||||
orderBy: [
|
||||
{ id: "desc" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export const database = new PrismaClient().$extends({
|
||||
model: {
|
||||
$allModels: {
|
||||
findPaginated<T, A>(
|
||||
this: T,
|
||||
pageData: pageData,
|
||||
fetchArgs: Prisma.Exact<A, Prisma.Args<T, "findMany">>,
|
||||
): Promise<Prisma.Result<T, A, "findMany">> {
|
||||
const context = Prisma.getExtensionContext(this) as any;
|
||||
switch (pageData.type) {
|
||||
case "before":
|
||||
return context.findMany(getBeforeParameters(pageData, fetchArgs));
|
||||
case "after":
|
||||
return context.findMany(getAfterParameters(pageData, fetchArgs));
|
||||
case "around":
|
||||
return Promise.all([
|
||||
context.findMany(getBeforeParameters({
|
||||
type: "before",
|
||||
id: pageData.id,
|
||||
count: Math.ceil(pageData.count),
|
||||
}, fetchArgs)),
|
||||
context.findMany(getAfterParameters({
|
||||
type: "after",
|
||||
id: pageData.id,
|
||||
count: Math.floor(pageData.count),
|
||||
}, fetchArgs)),
|
||||
]).then(rv => rv.flat()) as Promise<any>;
|
||||
case null:
|
||||
return context.findMany(getNullParameters(pageData, fetchArgs));
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
import { getCookie, H3Event } from "h3";
|
||||
|
||||
import { database, type data } from "./database";
|
||||
import { type user } from "~/utils/types/database";
|
||||
import { database } from "./database";
|
||||
|
||||
import { createError } from "#imports";
|
||||
|
||||
export default async function getRequestingUser(e: H3Event) {
|
||||
const cookie = getCookie(e, "token");
|
||||
const [[user]] = await database.query(
|
||||
["SELECT",
|
||||
"CONVERT(`users`.`id`, CHAR) as `id`,",
|
||||
"`users`.`username`,",
|
||||
"`users`.`email`,",
|
||||
"`users`.`display_name`",
|
||||
"FROM",
|
||||
"`sessions`",
|
||||
"LEFT JOIN `users` ON `sessions`.`user` = `users`.`id`",
|
||||
"WHERE `sessions`.`id` = ?",
|
||||
].join(" "),
|
||||
[cookie],
|
||||
) as data<user>;
|
||||
if (!cookie) throw createError("User not found");
|
||||
const { user } = await database.session.findUnique({
|
||||
where: {
|
||||
id: BigInt(cookie),
|
||||
},
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
display_name: true,
|
||||
email: true,
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}).then((e) => {
|
||||
if (e === null) throw createError("User not found");
|
||||
return e;
|
||||
});
|
||||
|
||||
if (!user) throw createError("User not found");
|
||||
return user;
|
||||
|
|
44
server/utils/prismaToWeb.ts
Normal file
44
server/utils/prismaToWeb.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { Decimal } from "@prisma/client/runtime/library";
|
||||
|
||||
type func = (...args: any[]) => any | Function;
|
||||
|
||||
export type replaceJsonUnparsableToString<T> =
|
||||
T extends Array<infer E> ? Array<replaceJsonUnparsableToString<E>>
|
||||
: {
|
||||
[K in keyof T]:
|
||||
T[K] extends null ? null
|
||||
: T[K] extends func ? never
|
||||
: T[K] extends Decimal ? `${number}`
|
||||
: T[K] extends Array<infer E> ? Array<replaceJsonUnparsableToString<E>>
|
||||
: T[K] extends object ? replaceJsonUnparsableToString<T[K]>
|
||||
: T[K] extends bigint ? `${bigint}`
|
||||
: T[K]
|
||||
};
|
||||
|
||||
type exactToInterface = (...args: any[]) => any extends Function ? true : false;
|
||||
|
||||
function arrayPrismaToWeb<T>(array: Array<T>) {
|
||||
return array.reduce(
|
||||
(pV, cV) => {
|
||||
pV.push(prismaToWeb(cV));
|
||||
return pV;
|
||||
},
|
||||
[] as Array<replaceJsonUnparsableToString<T>>,
|
||||
);
|
||||
}
|
||||
|
||||
export function prismaToWeb<T>(ivalue: T): replaceJsonUnparsableToString<T> {
|
||||
const rvalue: any = ivalue instanceof Array ? [] : {};
|
||||
|
||||
for (const i in ivalue) {
|
||||
const current = ivalue[i];
|
||||
if (current === null) rvalue[i] = null;
|
||||
else if (typeof current === 'function') continue;
|
||||
else if (current instanceof Decimal) rvalue[i] = current.toString();
|
||||
else if (current instanceof Array) rvalue[i] = arrayPrismaToWeb(current);
|
||||
else if (typeof current === 'object') rvalue[i] = prismaToWeb(current);
|
||||
else if (typeof current === 'bigint') rvalue[i] = current.toString();
|
||||
else rvalue[i] = current;
|
||||
}
|
||||
return rvalue;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue