- Remove the preset document suite (letters, emails, invoices, quotes, PDF generation, line items) with its routes, API handlers and tables - Rebrand to Cobble: site data, Lato, handoff design tokens (light and dark) in main.css, brand assets in public/brand, ember stone logo and regenerated favicons - Add bots, bot_messages, bot_tasks and bot_task_steps tables, regenerate DB types (BUN_SQLITE_COBBLE_*) and add a sample crew seed (seed:bots) - Add the chat shell at /admin/bots: rail with pinned coordinator and user popover, bottom-anchored conversation with composer, working-on panel, and a send-message API scoped to the session user - Add BotStone and Wordmark components, crew status and relative time utils with tests, and update AGENTS.md Co-Authored-By: Claude Opus 5.5 <[email protected]>
148 lines
5.3 KiB
TypeScript
148 lines
5.3 KiB
TypeScript
import { AppData } from "@/src/data/app-data";
|
|
import type { ApiReqParams, SQLInsertGenValueType } from "@/src/types";
|
|
import type { BUN_SQLITE_COBBLE_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_COBBLE_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_COBBLE_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,
|
|
};
|
|
}
|
|
}
|