import bcrypt from "bcryptjs"; import { CustomError } from "../utils"; import redisClient, * as redis from "../redis"; import knex from "../knex"; const selectable = [ "links.id", "links.address", "links.banned", "links.created_at", "links.domain_id", "links.updated_at", "links.password", "links.description", "links.expire_in", "links.target", "links.visit_count", "links.user_id", "links.uuid", "domains.address as domain" ]; const normalizeMatch = (match: Partial): Partial => { const newMatch = { ...match }; if (newMatch.address) { newMatch["links.address"] = newMatch.address; delete newMatch.address; } if (newMatch.user_id) { newMatch["links.user_id"] = newMatch.user_id; delete newMatch.user_id; } if (newMatch.uuid) { newMatch["links.uuid"] = newMatch.uuid; delete newMatch.uuid; } return newMatch; }; interface TotalParams { search?: string; } export const total = async (match: Match, params: TotalParams = {}) => { const query = knex("links"); Object.entries(match).forEach(([key, value]) => { query.andWhere(key, ...(Array.isArray(value) ? value : [value])); }); if (params.search) { query.andWhereRaw( "links.description || ' ' || links.address || ' ' || target ILIKE '%' || ? || '%'", [params.search] ); } const [{ count }]: { count: number }[] = await query.count("*"); return typeof count === "number" ? count : parseInt(count); }; interface GetParams { limit: number; search?: string; skip: number; } export const get = async (match: Partial, params: GetParams) => { const query = knex("links") .select(...selectable) .where(normalizeMatch(match)) .offset(params.skip) .limit(params.limit) .orderBy("created_at", "desc"); if (params.search) { query.andWhereRaw( "concat_ws(' ', description, links.address, target, domains.address) ILIKE '%' || ? || '%'", [params.search] ); } query.leftJoin("domains", "links.domain_id", "domains.id"); const links: LinkJoinedDomain[] = await query; return links; }; export const find = async (match: Partial): Promise => { if (match.address && match.domain_id) { const key = redis.key.link(match.address, match.domain_id); const cachedLink = await redisClient.get(key); if (cachedLink) return JSON.parse(cachedLink); } const link = await knex("links") .select(...selectable) .where(normalizeMatch(match)) .leftJoin("domains", "links.domain_id", "domains.id") .first(); if (link) { const key = redis.key.link(link.address, link.domain_id); redisClient.set(key, JSON.stringify(link), "EX", 60 * 60 * 2); } return link; }; interface Create extends Partial { address: string; target: string; } export const create = async (params: Create) => { let encryptedPassword: string = null; if (params.password) { const salt = await bcrypt.genSalt(12); encryptedPassword = await bcrypt.hash(params.password, salt); } const [link]: LinkJoinedDomain[] = await knex( "links" ).insert( { password: encryptedPassword, domain_id: params.domain_id || null, user_id: params.user_id || null, address: params.address, description: params.description || null, expire_in: params.expire_in || null, target: params.target }, "*" ); return link; }; export const remove = async (match: Partial) => { const link = await knex("links") .where(match) .first(); if (!link) { throw new CustomError("Link was not found."); } const deletedLink = await knex("links") .where("id", link.id) .delete(); redis.remove.link(link); return !!deletedLink; }; export const batchRemove = async (match: Match) => { const deleteQuery = knex("links"); const findQuery = knex("links"); Object.entries(match).forEach(([key, value]) => { findQuery.andWhere(key, ...(Array.isArray(value) ? value : [value])); deleteQuery.andWhere(key, ...(Array.isArray(value) ? value : [value])); }); const links = await findQuery; links.forEach(redis.remove.link); await deleteQuery.delete(); }; export const update = async (match: Partial, update: Partial) => { if (update.password) { const salt = await bcrypt.genSalt(12); update.password = await bcrypt.hash(update.password, salt); } const links = await knex("links") .where(match) .update({ ...update, updated_at: new Date().toISOString() }, "*"); links.forEach(redis.remove.link); return links; }; export const incrementVisit = async (match: Partial) => { return knex("links") .where(match) .increment("visit_count", 1); };