UNPKG

@cocalc/server

Version:

CoCalc server functionality: functions used by either the hub and the next.js server

136 lines (135 loc) 5.52 kB
"use strict"; /* Search for users. - by exact account_id - by exact email_address - by partial match on first_name and last_name - by @username */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const pool_1 = __importDefault(require("@cocalc/database/pool")); const misc_1 = require("@cocalc/util/misc"); const util_1 = require("@cocalc/database/postgres/util"); const logger_1 = require("@cocalc/backend/logger"); const logger = (0, logger_1.getLogger)("accounts/search"); async function search({ /* account_id,*/ query, limit, admin, }) { limit = limit ?? 20; admin = !!admin; logger.debug("search for ", query); // One special case: when the query is just an email address or uuid. // We just return that account or empty list if no match. if ((0, misc_1.isValidUUID)(query)) { logger.debug("get user by account_id"); const user = process(await getUserByAccountId(query), admin, false); return user ? [user] : []; } if ((0, misc_1.is_valid_email_address)(query)) { logger.debug("get user by email address"); const user = process(await getUserByEmailAddress(query), admin, true); return user ? [user] : []; } const { string_queries, email_queries } = (0, misc_1.parse_user_search)(query); if (admin) { // For admin we just do substring queries anyways. for (const email_address of email_queries) { string_queries.push([email_address]); } email_queries.splice(0, email_queries.length); // empty array } const results = []; let matches = await getUsersByEmailAddresses(email_queries, limit); for (const user of matches) { const x = process(user, admin, true); if (x) { results.push(x); } } matches = await getUsersByStringQueries(string_queries, admin, limit - matches.length); for (const user of matches) { const x = process(user, admin, false); if (x) { results.push(x); } } results.sort((a, b) => -(0, misc_1.cmp)(Math.max(a.last_active ?? 0, a.created ?? 0), Math.max(b.last_active ?? 0, b.created ?? 0))); return results; } exports.default = search; function process(user, admin = false, isEmailSearch) { if (user == null) return undefined; const x = { ...user }; if (x.email_address && x.email_address_verified) { x.email_address_verified = x.email_address_verified[x.email_address] != null; } if (!admin) { if (!isEmailSearch) { delete x.email_address; } delete x.banned; } (0, util_1.toEpoch)(x, ["last_active", "created"]); return x; } const FIELDS = " account_id, first_name, last_name, name, email_address, last_active, created, banned, email_address_verified "; async function getUserByEmailAddress(email_address) { const pool = (0, pool_1.default)("medium"); const { rows } = await pool.query(`SELECT ${FIELDS} FROM accounts WHERE email_address=$1`, [email_address.toLowerCase()]); return rows[0]; } async function getUserByAccountId(account_id) { const pool = (0, pool_1.default)("medium"); const { rows } = await pool.query(`SELECT ${FIELDS} FROM accounts WHERE account_id=$1`, [account_id.toLowerCase()]); return rows[0]; } async function getUsersByEmailAddresses(email_queries, limit) { logger.debug("getUsersByEmailAddresses", email_queries); if (email_queries.length == 0 || limit <= 0) return []; const pool = (0, pool_1.default)("medium"); const { rows } = await pool.query(`SELECT ${FIELDS} FROM accounts WHERE email_address = ANY($1::TEXT[]) AND deleted IS NULL`, [email_queries]); return rows; } async function getUsersByStringQueries(string_queries, admin, limit) { logger.debug("getUsersByStringQueries", string_queries); if (limit <= 0 || string_queries.length <= 0) { return []; } /* Substring search on first and last name, and for admin also email_address. With the two indexes, the query below is very fast, even on millions of accounts: CREATE INDEX accounts_first_name_idx ON accounts(first_name text_pattern_ops); CREATE INDEX accounts_last_name_idx ON accounts(last_name text_pattern_ops); */ const params = []; const where = []; let i = 1; for (const terms of string_queries) { const v = []; for (const s of terms) { v.push(`(lower(first_name) LIKE $${i}::TEXT OR lower(last_name) LIKE $${i}::TEXT OR '@' || lower(name) LIKE $${i}::TEXT ${admin ? `OR lower(email_address) LIKE $${i}::TEXT` : ""})`); params.push(`%${s}%`); i += 1; } where.push(`(${v.join(" AND ")})`); } let query = `SELECT ${FIELDS} FROM accounts WHERE deleted IS NOT TRUE AND (${where.join(" OR ")})`; if (!admin) { // Exclude unlisted users from search results query += " AND unlisted IS NOT true "; } // recently active users are much more relevant than old ones -- #2991 query += " ORDER BY COALESCE(last_active, created) DESC NULLS LAST"; query += ` LIMIT $${i}::INTEGER `; i += 1; params.push(limit); const pool = (0, pool_1.default)("medium"); const { rows } = await pool.query(query, params); return rows; } //# sourceMappingURL=search.js.map