@cocalc/server
Version:
CoCalc server functionality: functions used by either the hub and the next.js server
136 lines (135 loc) • 5.52 kB
JavaScript
;
/*
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