First Commit. Duplicate bunext-sqlite preset

This commit is contained in:
2026-10-18 16:47:11 +01:00
commit 81e4a8aecc
718 changed files with 41695 additions and 0 deletions
@@ -0,0 +1,69 @@
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
import type { TableType } from "@/src/types";
import uniqueStringArr from "@/src/utils/unique-string-arr";
import type { ServerQueryParam } from "@moduletrace/bun-sqlite";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
type Params = {
/**
* Allowed joins
*/
joins: TableType[];
query: ServerQueryParam<any>;
/**
* Allowed fields from joins
*/
include_fields?: (keyof BUN_SQLITE_BSQLITE_ALL_TYPEDEFS)[];
/**
* Fields to exclude from joins
*/
exclude_fields?: (keyof BUN_SQLITE_BSQLITE_ALL_TYPEDEFS)[];
};
export default function checkAllowedJoins({
joins,
query,
include_fields,
exclude_fields,
}: Params): APIResponseObject {
const all_joins = BunSQLite.utils.grab_join_fields_from_query_object({
query,
});
const all_joins_str_arr = uniqueStringArr(
all_joins
.filter((j) => Boolean(j.table))
.map((j) => j.table) as string[],
);
const are_joins_allowed =
_.intersection(joins, all_joins_str_arr).length >=
all_joins_str_arr.length;
if (!are_joins_allowed) {
return {
success: false,
msg: `Can't Join all tables: \`${all_joins_str_arr.join(", ")}\`. Tables allowed are \`${joins.join(", ")}\``,
};
}
if (include_fields?.[0]) {
for (let i = 0; i < all_joins.length; i++) {
const join = all_joins[i];
if (!join) continue;
const join_field = join.field as keyof BUN_SQLITE_BSQLITE_ALL_TYPEDEFS;
if (!include_fields.includes(join_field)) {
return {
success: false,
msg: `Can't Join field: \`${join_field}\` in \`${join.table}\` table`,
};
}
}
}
return {
success: true,
};
}
@@ -0,0 +1,41 @@
import type { BUN_SQLITE_BSQLITE_EMAIL_ATTACHMENTS } from "@/db/types/db";
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
import BunSQLite from "@moduletrace/bun-sqlite";
export type EmailAttachmentRecord = BUN_SQLITE_BSQLITE_EMAIL_ATTACHMENTS & {
letter_title?: string;
invoice_number?: string;
invoice_title?: string;
quote_number?: string;
quote_title?: string;
media_write_path?: string;
media_mime_type?: string;
};
export default async function grabEmailAttachments({
email_id,
}: {
email_id: number | string;
}): Promise<APIResponseObject<EmailAttachmentRecord>> {
return await BunSQLite.sql<EmailAttachmentRecord>({
sql: `
SELECT
ea.*,
l.title AS letter_title,
i.invoice_number,
i.title AS invoice_title,
q.quote_number,
q.title AS quote_title,
m.media_write_path,
m.mime_type AS media_mime_type
FROM email_attachments ea
LEFT JOIN letters l ON l.id = ea.letter_id
LEFT JOIN invoices i ON i.id = ea.invoice_id
LEFT JOIN quotes q ON q.id = ea.quote_id
LEFT JOIN media m ON m.id = ea.media_id
WHERE ea.email_id = ?
ORDER BY ea.id ASC
`,
values: [email_id],
});
}
@@ -0,0 +1,14 @@
import type { BUN_SQLITE_BSQLITE_EMAILS } from "@/db/types/db";
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
import BunSQLite from "@moduletrace/bun-sqlite";
export default async function grabEmail({
id,
}: {
id: number | string;
}): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_EMAILS>> {
return await BunSQLite.select<BUN_SQLITE_BSQLITE_EMAILS>({
table: "emails",
targetId: id,
});
}
@@ -0,0 +1,14 @@
import type { BUN_SQLITE_BSQLITE_LETTERS } from "@/db/types/db";
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
import BunSQLite from "@moduletrace/bun-sqlite";
export default async function grabLetter({
id,
}: {
id: number | string;
}): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_LETTERS>> {
return await BunSQLite.select<BUN_SQLITE_BSQLITE_LETTERS>({
table: "letters",
targetId: id,
});
}
@@ -0,0 +1,102 @@
import { Database } from "bun:sqlite";
import { beforeEach, describe, expect, test } from "bun:test";
import grabNextDocumentSequence, {
type DocumentNumberSqlRunner,
} from "./grab-next-document-sequence";
let db: Database;
let run_sql: DocumentNumberSqlRunner;
beforeEach(() => {
db = new Database(":memory:");
db.run("CREATE TABLE invoices (invoice_number TEXT UNIQUE)");
run_sql = async ({ sql, values }) => {
const row = db.query(sql).get(...values) as
| { max_seq: number | null }
| null;
return { singleRes: row };
};
});
function insertNumbers(numbers: string[]) {
const insert = db.prepare("INSERT INTO invoices (invoice_number) VALUES (?)");
for (const number of numbers) {
insert.run(number);
}
}
describe("grabNextDocumentSequence", () => {
test("starts at 1 for an empty table", async () => {
const next = await grabNextDocumentSequence({
table: "invoices",
column: "invoice_number",
prefix: "BSQLITE-202610",
run_sql,
});
expect(next).toBe(1);
});
test("returns max + 1 for existing numbers", async () => {
insertNumbers([
"BSQLITE-202610-001",
"BSQLITE-202610-002",
"BSQLITE-202610-003",
]);
const next = await grabNextDocumentSequence({
table: "invoices",
column: "invoice_number",
prefix: "BSQLITE-202610",
run_sql,
});
expect(next).toBe(4);
});
test("does not reuse a number after a deletion", async () => {
insertNumbers(["BSQLITE-202610-001", "BSQLITE-202610-003"]);
const next = await grabNextDocumentSequence({
table: "invoices",
column: "invoice_number",
prefix: "BSQLITE-202610",
run_sql,
});
expect(next).toBe(4);
});
test("ignores numbers from other prefixes", async () => {
insertNumbers([
"BSQLITE-202609-099",
"BSQLITEQ-202610-050",
"BSQLITE-202610-002",
]);
const next = await grabNextDocumentSequence({
table: "invoices",
column: "invoice_number",
prefix: "BSQLITE-202610",
run_sql,
});
expect(next).toBe(3);
});
test("handles sequences beyond three digits", async () => {
insertNumbers(["BSQLITE-202610-009", "BSQLITE-202610-010"]);
const next = await grabNextDocumentSequence({
table: "invoices",
column: "invoice_number",
prefix: "BSQLITE-202610",
run_sql,
});
expect(next).toBe(11);
});
});
@@ -0,0 +1,41 @@
import BunSQLite from "@moduletrace/bun-sqlite";
type DocumentNumberTarget = {
table: "invoices" | "quotes";
column: "invoice_number" | "quote_number";
};
export type DocumentNumberSqlRunner = (args: {
sql: string;
values: (string | number)[];
}) => Promise<{ singleRes?: { max_seq?: number | string | null } | null }>;
type Params = DocumentNumberTarget & {
prefix: string;
/**
* Injectable query runner, used by tests to target an in-memory DB.
*/
run_sql?: DocumentNumberSqlRunner;
};
/**
* # Grab the next document sequence.
* Reads the highest existing numeric suffix for the prefix instead of
* counting rows, so deleting a document can never hand out a number that
* already exists.
*/
export default async function grabNextDocumentSequence({
table,
column,
prefix,
run_sql,
}: Params): Promise<number> {
const sql = `SELECT MAX(CAST(SUBSTR(${column}, ?) AS INTEGER)) AS max_seq FROM ${table} WHERE ${column} LIKE ?`;
const values = [prefix.length + 2, `${prefix}-%`];
const res = run_sql
? await run_sql({ sql, values })
: await BunSQLite.sql({ sql, values });
return Number(res.singleRes?.max_seq || 0) + 1;
}
@@ -0,0 +1,64 @@
import type {
BUN_SQLITE_BSQLITE_INVOICE_ITEMS,
BUN_SQLITE_BSQLITE_INVOICE_QUOTES,
BUN_SQLITE_BSQLITE_INVOICES,
} from "@/db/types/db";
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
import BunSQLite from "@moduletrace/bun-sqlite";
export default async function grabInvoice({
id,
}: {
id: number | string;
}): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_INVOICES>> {
return await BunSQLite.select<BUN_SQLITE_BSQLITE_INVOICES>({
table: "invoices",
targetId: id,
});
}
export async function grabInvoiceItems({
invoice_id,
}: {
invoice_id: number | string;
}): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_INVOICE_ITEMS>> {
return await BunSQLite.select<BUN_SQLITE_BSQLITE_INVOICE_ITEMS>({
table: "invoice_items",
query: {
query: {
invoice_id: {
value: Number(invoice_id),
},
},
order: [
{ field: "sort_order", strategy: "ASC" },
{ field: "id", strategy: "ASC" },
],
},
});
}
export type InvoiceQuoteLinkRecord = BUN_SQLITE_BSQLITE_INVOICE_QUOTES & {
quote_number?: string;
quote_title?: string;
};
export async function grabInvoiceQuoteLinks({
invoice_id,
}: {
invoice_id: number | string;
}): Promise<APIResponseObject<InvoiceQuoteLinkRecord>> {
return await BunSQLite.sql<InvoiceQuoteLinkRecord>({
sql: `
SELECT
iq.*,
q.quote_number,
q.title AS quote_title
FROM invoice_quotes iq
LEFT JOIN quotes q ON q.id = iq.quote_id
WHERE iq.invoice_id = ?
ORDER BY iq.id ASC
`,
values: [invoice_id],
});
}
@@ -0,0 +1,55 @@
import type { BUN_SQLITE_BSQLITE_MEDIA_JOIN } from "@/src/types/sql-joins";
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
import _ from "lodash";
import grabUsers from "../users/grab-users";
type Params = {
media_res: APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA_JOIN>;
};
export default async function appendUsersToMedia({
media_res,
}: Params): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA_JOIN>> {
try {
let new_media_res = _.cloneDeep(media_res);
const media = new_media_res.payload;
const users_ids = media
?.map((m) => m.user_id)
.filter((n) => !_.isUndefined(n) && _.isNumber(n));
const unique_users_ids = _.uniq(users_ids);
if (users_ids?.[0] && media?.[0]) {
const users_res = await grabUsers({
query: { ids: unique_users_ids },
});
const users = users_res.payload || [];
for (let i = 0; i < media.length; i++) {
const single_media = media[i];
const target_user = users.find(
(u) => u.id == single_media?.user_id,
);
if (!target_user || !single_media?.user_id) {
continue;
}
new_media_res.payload![i]!.user = target_user;
if (i == 0) {
new_media_res.singleRes = target_user;
}
}
}
return new_media_res;
} catch (error: any) {
console.log("Append users to media ERROR =>", error.message);
return media_res;
}
}
@@ -0,0 +1,113 @@
import _ from "lodash";
import type { BUN_SQLITE_BSQLITE_MEDIA } from "@/db/types/db";
import grabMediaFromParadigm from "@/src/functions/backend/db/media/grab-media-from-paraidigm";
import type { MediaParadigmType, TableType, User } from "@/src/types";
import grabDirNames from "@/src/utils/grab-dir-names";
import BunSQLite from "@moduletrace/bun-sqlite";
import { rmSync } from "fs";
import path from "path";
type Params = {
id?: string | number;
ids?: (string | number)[];
media_paradigm?: MediaParadigmType;
/**
* ID of the user to be assigned to this media.
* Different from logged in user id
*/
user_id?: string | number | null;
can_delete_all_media?: boolean;
};
export default async function deleteMedia(params: Params) {
const { id, ids, media_paradigm, user_id, can_delete_all_media } = params;
const { DATA_DIR } = grabDirNames();
let media_to_delete = new Set<number>();
if (media_paradigm && user_id) {
const media_res = (
await grabMediaFromParadigm({ media_paradigm, user_id })
).payload;
if (media_res) {
for (let i = 0; i < media_res.length; i++) {
const media = media_res[i];
if (!media?.id) continue;
media_to_delete.add(media.id);
}
}
}
let media_ids_to_del: (number | string)[] = [];
if (id) {
media_ids_to_del.push(id);
}
if (ids) {
media_ids_to_del.push(...ids);
}
if (media_ids_to_del[0]) {
for (let i = 0; i < media_ids_to_del.length; i++) {
const media_id_to_delete = media_ids_to_del[i];
const target_media = (
await BunSQLite.select<BUN_SQLITE_BSQLITE_MEDIA>({
table: "media",
targetId: media_id_to_delete,
})
).singleRes;
if (target_media?.id) {
if (target_media.user_id !== user_id && !can_delete_all_media) {
throw new Error(`Can't delete this media!`);
}
media_to_delete.add(target_media.id);
}
}
}
const media_to_delete_array = Array.from(media_to_delete);
for (let i = 0; i < media_to_delete_array.length; i++) {
const media_id = media_to_delete_array[i];
const target_media = (
await BunSQLite.select<BUN_SQLITE_BSQLITE_MEDIA>({
table: "media",
targetId: media_id,
})
).singleRes;
if (!target_media?.id) {
continue;
}
if (target_media.media_write_path) {
rmSync(
path.join(DATA_DIR, target_media.media_write_path),
{ force: true },
);
}
if (target_media.media_thumbnail_write_path) {
rmSync(
path.join(DATA_DIR, target_media.media_thumbnail_write_path),
{ force: true },
);
}
await BunSQLite.delete<BUN_SQLITE_BSQLITE_MEDIA, TableType>({
table: "media",
targetId: target_media.id,
});
}
return {
success: true,
};
}
@@ -0,0 +1,80 @@
import type { BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS } from "@/db/types/db";
import type { MediaParadigmType, TableType } from "@/src/types";
import type { BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS_JOIN } from "@/src/types/sql-joins";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
type Params = {
media_paradigm: MediaParadigmType;
user_id?: string | number;
};
export default async function grabMediaFromParadigm({
media_paradigm,
user_id,
}: Params): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS_JOIN>> {
try {
const media_paradigms_res = await BunSQLite.select<
BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS,
TableType
>({
table: "media_paradigms",
query: {
query: {
media_paradigm: {
value: media_paradigm,
},
},
join: [
{
joinType: "INNER JOIN",
tableName: "media",
match: [
{
source: "media_id",
target: "id",
},
],
selectFields: ["user_id"],
},
{
joinType: "INNER JOIN",
tableName: "users",
match: [
{
source: {
fieldName: "user_id",
tableName: "media",
},
target: "id",
},
...(user_id
? [
{
source: {
fieldName: "id",
tableName: "users",
},
targetLiteral: user_id,
},
]
: []),
],
selectFields: ["first_name", "last_name"],
},
],
},
});
return {
...media_paradigms_res,
singleRes: media_paradigms_res.singleRes,
};
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
}
@@ -0,0 +1,134 @@
import type { BUN_SQLITE_BSQLITE_MEDIA } from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import type { ApiReqParams, User } from "@/src/types";
import type { BUN_SQLITE_BSQLITE_MEDIA_JOIN } from "@/src/types/sql-joins";
import BunSQLite, { type APIResponseObject } from "@moduletrace/bun-sqlite";
import type { SQLInsertGenValueType } from "@moduletrace/bun-sqlite";
import _ from "lodash";
import appendUsersToMedia from "./append-users-to-media";
type Params = {
query?: ApiReqParams;
user?: User;
};
export default async function grabMedia(
params?: Params,
): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA_JOIN>> {
const { query, user } = params || {};
try {
const LIMIT = AppData["DefaultQueryLimit"];
const values: SQLInsertGenValueType[] = [];
const hasParadigms = Boolean(query?.media_paradigms?.length);
let sql = `SELECT\n`;
sql += ` m.*,\n`;
sql += ` GROUP_CONCAT(DISTINCT mp.media_paradigm) AS media_paradigms\n`;
sql += `FROM media m\n`;
sql += `LEFT JOIN media_paradigms mp ON m.id = mp.media_id\n`;
sql += `WHERE COALESCE(m.archived, 0) <> 1\n`;
if (hasParadigms && query?.media_paradigms) {
const placeholders = query.media_paradigms.map(() => "?").join(",");
sql += ` AND m.id IN (\n`;
sql += ` SELECT sub_mp.media_id\n`;
sql += ` FROM media_paradigms sub_mp\n`;
sql += ` WHERE sub_mp.media_paradigm IN (${placeholders})\n`;
sql += ` GROUP BY sub_mp.media_id\n`;
sql += ` HAVING COUNT(DISTINCT sub_mp.media_paradigm) = ?\n`;
sql += ` )\n`;
values.push(...query.media_paradigms, query.media_paradigms.length);
}
if (query?.is_media_private) {
sql += ` AND m.is_private = 1\n`;
if (!user?.id) {
throw new Error(`Private media requires authenticated user`);
} else {
sql += ` AND m.user_id = ?\n`;
values.push(user.id);
}
} else {
sql += ` AND COALESCE(m.is_private, 0) <> 1\n`;
}
if (query?.id) {
sql += ` AND m.id = ?\n`;
values.push(query.id);
}
if (query?.is_media_primary) {
sql += ` AND m.is_primary = ?\n`;
values.push(1);
}
if (query?.user_id) {
sql += ` AND m.user_id = ?\n`;
values.push(query.user_id);
}
// 4. Parameterized Search Terms
if (query?.search_terms?.length) {
const fields_to_search: (keyof BUN_SQLITE_BSQLITE_MEDIA)[] = [
"title",
"description",
"summary",
"text_content",
];
const searchConditions: string[] = [];
for (const search_term of query.search_terms) {
const termConditions: string[] = [];
for (const field of fields_to_search) {
termConditions.push(`m.${field} LIKE ?`);
values.push(`%${search_term}%`);
}
searchConditions.push(`(${termConditions.join(" OR ")})`);
}
sql += ` AND (${searchConditions.join(" OR ")})\n`;
}
sql += `GROUP BY m.id\n`;
sql += `ORDER BY m.id DESC\n`;
let sql_limit = `LIMIT ?`;
const limitValues: SQLInsertGenValueType[] = [LIMIT];
if (query?.page && query.page > 1) {
const offset = LIMIT * (query.page - 1);
sql_limit += ` OFFSET ?`;
limitValues.push(offset);
}
const final_sql = `${sql}\n${sql_limit}`;
const final_values = [...values, ...limitValues];
const res = await BunSQLite.sql<BUN_SQLITE_BSQLITE_MEDIA_JOIN>({
sql: final_sql,
values: final_values,
});
// Count Query
if (query?.count) {
const count_sql = `SELECT COUNT(*) AS count FROM (${sql}) AS subquery_count`;
const count_res = await BunSQLite.sql<{ count: number }>({
sql: count_sql,
values,
});
res.count = count_res.singleRes?.count ?? 0;
}
return await appendUsersToMedia({ media_res: res });
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
}
@@ -0,0 +1,38 @@
import type {
BUN_SQLITE_BSQLITE_QUOTE_ITEMS,
BUN_SQLITE_BSQLITE_QUOTES,
} from "@/db/types/db";
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
import BunSQLite from "@moduletrace/bun-sqlite";
export default async function grabQuote({
id,
}: {
id: number | string;
}): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_QUOTES>> {
return await BunSQLite.select<BUN_SQLITE_BSQLITE_QUOTES>({
table: "quotes",
targetId: id,
});
}
export async function grabQuoteItems({
quote_id,
}: {
quote_id: number | string;
}): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_QUOTE_ITEMS>> {
return await BunSQLite.select<BUN_SQLITE_BSQLITE_QUOTE_ITEMS>({
table: "quote_items",
query: {
query: {
quote_id: {
value: Number(quote_id),
},
},
order: [
{ field: "sort_order", strategy: "ASC" },
{ field: "id", strategy: "ASC" },
],
},
});
}
@@ -0,0 +1,32 @@
import type {
BUN_SQLITE_BSQLITE_USER_TYPES,
BunSQLiteTables,
} from "@/db/types/db";
import BunSQLite from "@moduletrace/bun-sqlite";
type Params = {
user_id: number | string;
};
/**
* All `user_types` rows for a user (including `revoked`).
*/
export default async function grabUserTypes({
user_id,
}: Params): Promise<BUN_SQLITE_BSQLITE_USER_TYPES[]> {
const res = await BunSQLite.select<
BUN_SQLITE_BSQLITE_USER_TYPES,
(typeof BunSQLiteTables)[number]
>({
table: "user_types",
query: {
query: {
user_id: {
value: user_id,
},
},
},
});
return res.payload || [];
}
@@ -0,0 +1,147 @@
import { AppData } from "@/src/data/app-data";
import type { ApiReqParams, SQLInsertGenValueType } from "@/src/types";
import type { BUN_SQLITE_BSQLITE_USERS_JOIN } from "@/src/types/sql-joins";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import _ from "lodash";
type Params = {
query?: ApiReqParams;
/**
* For search terms (ie `search_term_*`) parameters.
* This forces all searches to `=` instead of `LIKE`
*/
exact_match?: boolean;
user_id?: string | number;
};
export default async function grabUsers(
params?: Params,
): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_USERS_JOIN>> {
const { query } = params || {};
try {
const LIMIT = AppData["DefaultQueryLimit"];
let sql = ``;
let values: SQLInsertGenValueType[] = [];
sql += `SELECT\n`;
sql += ` u.*,\n`;
sql += ` ut_agg.user_types,\n`;
sql += ` m.id AS profile_media_id\n`;
sql += `FROM users u\n`;
sql += `LEFT JOIN (\n`;
sql += ` SELECT user_id, GROUP_CONCAT(DISTINCT user_type) AS user_types\n`;
sql += ` FROM user_types\n`;
sql += ` GROUP BY user_id\n`;
sql += `) ut_agg ON u.id = ut_agg.user_id\n`;
sql += `LEFT JOIN media m ON (\n`;
sql += ` u.id = m.user_id\n`;
sql += ` AND m.is_primary = 1\n`;
sql += `)\n`;
sql += `LEFT JOIN media_paradigms mp ON (\n`;
sql += ` m.id = mp.media_id\n`;
sql += ` AND mp.media_paradigm = 'user-profile-image'\n`;
sql += `)\n`;
sql += `WHERE COALESCE(u.archived, 0) <> 1\n`;
const target_user_id = params?.user_id || query?.user_id;
if (target_user_id) {
sql += ` AND u.id=?\n`;
values.push(target_user_id);
}
if (query?.search_term_user_type) {
sql += ` AND EXISTS (\n`;
sql += ` SELECT 1 FROM user_types WHERE user_id = u.id AND user_type LIKE`;
if (Array.isArray(query.search_term_user_type)) {
sql += `(`;
for (let i = 0; i < query.search_term_user_type.length; i++) {
const ut = query.search_term_user_type[i];
sql += ` ?`;
values.push(`%${ut}%`);
}
sql += `)`;
} else {
sql += ` ?`;
values.push(`%${query.search_term_user_type}%`);
}
sql += ` )\n`;
} else if (query?.target_user_type) {
sql += ` AND EXISTS (
SELECT 1 FROM user_types WHERE user_id = u.id AND user_type = ?
)\n`;
values.push(query.target_user_type);
}
let search_terms = [
{ field: "first_name", value: query?.search_term_first_name },
{ field: "last_name", value: query?.search_term_last_name },
{ field: "email", value: query?.search_term_email },
{ field: "id", value: query?.search_term_id },
] as const;
for (const term of search_terms) {
if (term.value) {
const isId = term.field === "id";
const match_value = params?.exact_match ? "=" : "LIKE";
if (Array.isArray(term.value)) {
sql += ` AND (`;
for (let i = 0; i < term.value.length; i++) {
const term_value = term.value[i];
if (!term_value) continue;
const match_value_param = params?.exact_match
? term_value
: `%${term_value.replace(/ /g, "%")}%`;
sql += ` u.${term.field} ${isId ? "=" : match_value} ?`;
if (i < term.value.length - 1) {
sql += ` OR`;
}
values.push(isId ? term_value : match_value_param);
}
sql += ` )\n`;
} else {
const match_value_param = params?.exact_match
? term.value
: `%${term.value.replace(/ /g, "%")}%`;
sql += ` AND u.${term.field} ${isId ? "=" : match_value} ?\n`;
values.push(isId ? term.value : match_value_param);
}
}
}
let sql_limit = ``;
sql_limit += `LIMIT ${LIMIT}\n`;
if (query?.page) {
const offset = LIMIT * (query.page - 1);
sql_limit += `OFFSET ${offset}\n`;
}
const res = await BunSQLite.sql<BUN_SQLITE_BSQLITE_USERS_JOIN>({
sql: `${sql}\n${sql_limit}`,
values,
});
if (query?.count) {
const count_sql = `SELECT COUNT(*) AS count FROM (${sql}) x`;
// const count_sql = `SELECT COUNT(*) AS count FROM users u WHERE u.archived IS NOT 1 ${
// query?.user_id ? "AND u.id = ?" : ""
// }`;
const count_res = await BunSQLite.sql({ sql: count_sql, values });
res.count = count_res.singleRes?.count;
}
return res;
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
}
@@ -0,0 +1,84 @@
import TurndownService from "turndown";
import { extractText, getDocumentProxy } from "unpdf";
import mammoth from "mammoth";
// import * as XLSX from "xlsx";
import bufferFromUrl from "@/src/utils/buffer-from-url";
const turndownService = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
});
type Params = {
file_stream?: Buffer<ArrayBuffer>;
file_path?: string;
/**
* File extension, e.g. `pdf`, `docx`, `xlsx`
*/
file_type?: string;
url?: string;
};
export default async function fileToText({
file_type,
file_stream,
file_path,
url,
}: Params): Promise<string | undefined> {
let buffer: Buffer | undefined;
let ext: string;
if (url) {
const result = await bufferFromUrl(url);
buffer = result.buffer;
ext = result.ext;
} else {
buffer = file_path
? Buffer.from(await Bun.file(file_path).arrayBuffer())
: file_stream;
ext = (file_type || file_path?.split(".").pop() || "txt")
.toString()
.toLowerCase();
}
if (!buffer) return undefined;
switch (ext) {
case "docx": {
const { value: html } = await mammoth.convertToHtml({ buffer });
return turndownService.turndown(html);
}
case "pdf": {
try {
const pdfUintBuffer = new Uint8Array(buffer);
const pdfDocProxy = await getDocumentProxy(pdfUintBuffer);
const { text } = await extractText(pdfDocProxy, {
mergePages: true,
});
return text;
} catch (err: any) {
console.error(
`PDF text extraction failed: ${err?.message ?? err}`,
);
throw new Error(
`Failed to extract text from PDF: ${err?.message ?? "unknown error"}`,
);
}
}
// case "xlsx":
// case "csv": {
// const workbook = XLSX.read(buffer, { type: "buffer" });
// return workbook.SheetNames.map((name) => {
// const sheet = workbook.Sheets[name];
// return `### Sheet: ${name}\n\n${XLSX.utils.sheet_to_csv(sheet!)}`;
// }).join("\n\n");
// }
case "md":
case "txt":
default:
return buffer.toString("utf-8");
}
}
@@ -0,0 +1,27 @@
import type { MediaDataType } from "@/src/types";
export default async function MediaDataToArrayBuffer(
body: MediaDataType,
): Promise<ArrayBuffer> {
if (body == null) {
return new ArrayBuffer(0);
}
if (body instanceof ArrayBuffer) {
return body;
}
if (body instanceof SharedArrayBuffer) {
const copy = new ArrayBuffer(body.byteLength);
new Uint8Array(copy).set(new Uint8Array(body));
return copy;
}
if (typeof body === "string") {
return new TextEncoder().encode(body).buffer as ArrayBuffer;
}
if (body instanceof ReadableStream) {
return Bun.readableStreamToArrayBuffer(body);
}
// Blob, BunFile, Request, Response — wrap in Response and use Bun's reader
return new Response(body as BodyInit)
.bytes()
.then((b) => b.buffer as ArrayBuffer);
}
@@ -0,0 +1,24 @@
import type { MediaDataType } from "@/src/types";
export default async function mediaDataToSharpInput(
body: MediaDataType,
): Promise<Buffer> {
if (body instanceof Buffer) return body;
if (body instanceof SharedArrayBuffer)
return Buffer.from(new Uint8Array(body));
if (body instanceof ArrayBuffer) return Buffer.from(body);
if (body instanceof Uint8Array) return Buffer.from(body);
if (body instanceof Blob || body instanceof File)
return Buffer.from(await body.arrayBuffer());
if (body instanceof Response || body instanceof Request)
return Buffer.from(await body.arrayBuffer());
if (typeof body === "string") return Buffer.from(body, "base64");
if (ArrayBuffer.isView(body))
return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
if ("arrayBuffer" in body && typeof body.arrayBuffer === "function") {
return Buffer.from(await (body as any).arrayBuffer());
}
throw new Error(`Unsupported body type: ${typeof body}`);
}
@@ -0,0 +1,50 @@
import type { MediaDataType } from "@/src/types";
import MediaDataToArrayBuffer from "./media-data-to-array-buffer";
export default async function sniffMediaExtension(
data: MediaDataType,
): Promise<string | undefined> {
try {
const bytes = new Uint8Array(await MediaDataToArrayBuffer(data));
if (bytes.length < 12) return undefined;
const has = (...signature: number[]) =>
signature.every((byte, i) => bytes[i] === byte);
if (has(0xff, 0xd8, 0xff)) return "jpg";
if (has(0x89, 0x50, 0x4e, 0x47)) return "png";
if (has(0x47, 0x49, 0x46, 0x38)) return "gif";
if (has(0x25, 0x50, 0x44, 0x46)) return "pdf";
if (has(0x50, 0x4b, 0x03, 0x04)) return "zip";
if (has(0x49, 0x44, 0x33)) return "mp3";
if (has(0x4f, 0x67, 0x67, 0x53)) return "ogg";
if (has(0x52, 0x49, 0x46, 0x46)) {
const riff_type = String.fromCharCode(
bytes[8]!,
bytes[9]!,
bytes[10]!,
bytes[11]!,
);
if (riff_type === "WEBP") return "webp";
if (riff_type === "WAVE") return "wav";
}
if (
String.fromCharCode(bytes[4]!, bytes[5]!, bytes[6]!, bytes[7]!) ===
"ftyp"
) {
return "mp4";
}
const head = String.fromCharCode(...bytes.subarray(0, 8)).trimStart();
if (head.startsWith("<svg") || head.startsWith("<?xml")) return "svg";
return undefined;
} catch (error) {
return undefined;
}
}
@@ -0,0 +1,300 @@
import type {
BUN_SQLITE_BSQLITE_MEDIA,
BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS,
BUN_SQLITE_BSQLITE_USERS,
BunSQLiteTables,
} from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import type {
MediaDataType,
MediaParadigm,
MediaType,
TableType,
User,
} from "@/src/types";
import grabDirNames from "@/src/utils/grab-dir-names";
import BunSQLite, { type APIResponseObject } from "@moduletrace/bun-sqlite";
import { existsSync, mkdirSync } from "fs";
import path from "path";
import sharp from "sharp";
import fileToText from "./file-to-text";
import MediaDataToArrayBuffer from "./media-data-to-array-buffer";
import sniffMediaExtension from "./sniff-media-ext";
import mediaDataToSharpInput from "./meida-data-to-sharp-input";
import deleteMedia from "../../media/delete-media";
type Params = {
media_paradigms?: MediaParadigm[];
media_type?: MediaType;
user: BUN_SQLITE_BSQLITE_USERS | User;
data?: MediaDataType;
is_primary?: boolean;
update_on_duplicate?: boolean;
extract_media_text_content?: boolean;
media?: BUN_SQLITE_BSQLITE_MEDIA;
media_name?: string;
media_mime_type?: string;
is_private?: boolean;
image_width?: number;
image_height?: number;
thumbnail_width?: number;
existing_media_id?: string | number;
media_text_content?: string;
};
export default async function uploadAndRecordMedia({
media_paradigms,
user,
data,
media_type,
is_primary,
update_on_duplicate,
extract_media_text_content,
media,
media_mime_type,
media_name,
is_private,
image_width,
image_height,
thumbnail_width,
existing_media_id,
media_text_content: passed_media_text_content,
}: Params): Promise<APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA>> {
const now = Date.now();
let target_file_name = `${media_name || now}`;
let target_file_thumbnail_name = `${media_name || now}-thumb`;
const {
USER_MEDIA_PRIVATE_RELATIVE_DIR,
USER_MEDIA_PUBLIC_RELATIVE_DIR,
DATA_DIR,
} = grabDirNames({
user,
});
const MEDIA_RELATIVE_DIR = is_private
? USER_MEDIA_PRIVATE_RELATIVE_DIR
: USER_MEDIA_PUBLIC_RELATIVE_DIR;
if (!MEDIA_RELATIVE_DIR) {
throw new Error(`No target media directory found!`);
}
let media_written_to_disk = false;
let media_text_content = passed_media_text_content;
let media_ext = media_mime_type
? media_mime_type.replace(/\./g, "")
: undefined;
if (!media_ext && data) {
if (typeof data === "string" && data.startsWith("data:")) {
const base64_mime = data.match(/^data:([^;]+);base64,/)?.[1];
if (base64_mime) {
media_ext = base64_mime.split("/").pop() ?? undefined;
}
} else if (data instanceof File && data.name.includes(".")) {
media_ext = data.name.split(".").pop();
} else if (
data instanceof Blob &&
data.type &&
!data.type.includes("octet-stream")
) {
media_ext = data.type.split("/").pop();
} else if (media_type === "image") {
media_ext = "jpeg";
} else if (typeof data !== "string") {
media_ext = await sniffMediaExtension(data);
}
}
const media_write_path = path.join(
MEDIA_RELATIVE_DIR,
`${target_file_name}.${media_ext}`,
);
const media_thumbnail_write_path = path.join(
MEDIA_RELATIVE_DIR,
`${target_file_thumbnail_name}.${media_ext}`,
);
const media_write_path_full = path.join(DATA_DIR, media_write_path);
const media_thumbnail_write_path_full = path.join(
DATA_DIR,
media_thumbnail_write_path,
);
mkdirSync(path.dirname(media_write_path_full), { recursive: true });
if (data && media_type == "image" && media_ext) {
const buffer = await mediaDataToSharpInput(data);
const is_profile_image =
media_paradigms?.includes("user-profile-image");
const thumb_width = thumbnail_width ?? AppData["ImageThumbnailSize"];
let full_width: number | undefined;
let full_height: number | undefined;
let full_options: {
fit?: "cover" | "inside";
withoutEnlargement?: boolean;
} = {};
if (is_profile_image) {
full_width = AppData["MainImageSize"];
full_height = AppData["MainImageSize"];
full_options = { fit: "cover" };
} else {
const meta = await sharp(buffer).metadata();
const source_width = image_width ?? meta.width;
const source_height = image_height ?? meta.height;
const max = AppData["MaxImageWidth"];
full_width =
source_width != null
? source_width > max
? max
: source_width
: max;
full_height =
image_height != null
? source_height != null && source_height > max
? max
: source_height
: undefined;
full_options = { fit: "inside", withoutEnlargement: true };
}
const [full, thumb] = await Promise.all([
sharp(buffer)
.resize(full_width, full_height, full_options)
.jpeg({ quality: AppData["ImageQuality"] })
.toBuffer(),
sharp(buffer)
.resize(
thumb_width,
is_profile_image ? thumb_width : undefined,
is_profile_image
? { fit: "cover" }
: { fit: "inside", withoutEnlargement: true },
)
.jpeg({ quality: AppData["ImageThumbnailQuality"] })
.toBuffer(),
]);
await Bun.write(media_write_path_full, full);
await Bun.write(media_thumbnail_write_path_full, thumb);
} else if (data) {
const media_array_buffer = await MediaDataToArrayBuffer(data);
await Bun.write(media_write_path_full, media_array_buffer, {
mode: 1,
});
if (extract_media_text_content) {
const media_text = await fileToText({
file_type: media_ext,
file_stream: Buffer.from(media_array_buffer),
});
if (media_text) {
media_text_content = media_text;
}
}
}
if (existsSync(media_write_path)) {
media_written_to_disk = true;
}
if (is_primary) {
const existing_user_primary_media = await BunSQLite.select<
BUN_SQLITE_BSQLITE_MEDIA,
TableType
>({
table: "media",
query: {
query: {
user_id: { value: user.id },
is_primary: { value: 1 },
},
},
});
if (existing_user_primary_media.payload) {
await deleteMedia({
can_delete_all_media: true,
ids: existing_user_primary_media.payload.map((m) => m.id!),
});
}
}
const media_data: BUN_SQLITE_BSQLITE_MEDIA = {
...media,
user_id: Number(user.id),
is_primary: is_primary ? 1 : 0,
media_write_path: media_write_path,
media_thumbnail_write_path:
media_type === "image" ? media_thumbnail_write_path : undefined,
media_type: media_type as any,
mime_type: media_type == "image" ? "jpeg" : media_ext,
text_content: media_text_content,
};
let media_record: BUN_SQLITE_BSQLITE_MEDIA | undefined = undefined;
if (existing_media_id) {
const update_media_record = await BunSQLite.update<
BUN_SQLITE_BSQLITE_MEDIA,
TableType
>({
data: media_data,
table: "media",
targetId: existing_media_id,
});
media_record = update_media_record.singleRes || undefined;
} else {
const new_media_record = await BunSQLite.insert<
BUN_SQLITE_BSQLITE_MEDIA,
TableType
>({
data: [media_data],
table: "media",
update_on_duplicate,
});
media_record = new_media_record.singleRes || undefined;
}
if (!media_record) {
throw new Error(`Couldn't record media!`);
}
if (media_paradigms?.[0] && media_record?.id) {
const delete_existing_paradigms = await BunSQLite.delete<
BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS,
(typeof BunSQLiteTables)[number]
>({
table: "media_paradigms",
query: {
query: { media_id: { value: media_record.id } },
},
});
await BunSQLite.insert<
BUN_SQLITE_BSQLITE_MEDIA_PARADIGMS,
(typeof BunSQLiteTables)[number]
>({
data: media_paradigms.map((mp) => ({
media_id: media_record.id,
media_paradigm: mp,
})),
table: "media_paradigms",
update_on_duplicate,
});
}
return {
success: true,
singleRes: media_record,
};
}
@@ -0,0 +1,28 @@
import sanitizeRichTextHtml from "@/src/utils/sanitize-rich-text-html";
type UserWithBio = {
bio?: string | null;
};
type Params<T extends UserWithBio> = {
user: T;
};
/**
* # Sanitize a user's `bio` HTML.
* The `bio` column is editable by every logged-in user (including
* `read_only`) and rendered with `dangerouslySetInnerHTML`, so it must be
* sanitized on write and before render.
*/
export default function sanitizeUserBio<T extends UserWithBio>({
user,
}: Params<T>): T {
if (typeof user.bio !== "string" || !user.bio) {
return user;
}
return {
...user,
bio: sanitizeRichTextHtml({ html: user.bio }),
} as T;
}