2023-01-11 03:41:53 -05:00
|
|
|
import type { User, CreateUser } from '@logto/schemas';
|
2023-01-04 02:39:27 -05:00
|
|
|
import { SearchJointMode, Users } from '@logto/schemas';
|
2022-10-21 00:14:17 -05:00
|
|
|
import type { OmitAutoSetFields } from '@logto/shared';
|
|
|
|
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
|
2023-01-09 03:41:14 -05:00
|
|
|
import type { CommonQueryMethods } from 'slonik';
|
2021-07-02 08:14:18 -05:00
|
|
|
import { sql } from 'slonik';
|
2021-08-29 22:30:54 -05:00
|
|
|
|
2023-01-09 03:41:14 -05:00
|
|
|
import { buildUpdateWhereWithPool } from '#src/database/update-where.js';
|
2022-11-21 03:38:24 -05:00
|
|
|
import { DeletionError } from '#src/errors/SlonikError/index.js';
|
2022-12-14 03:36:57 -05:00
|
|
|
import type { Search } from '#src/utils/search.js';
|
|
|
|
import { buildConditionsFromSearch } from '#src/utils/search.js';
|
2021-07-02 08:14:18 -05:00
|
|
|
|
|
|
|
const { table, fields } = convertToIdentifiers(Users);
|
|
|
|
|
2023-01-09 03:58:02 -05:00
|
|
|
export const createUserQueries = (pool: CommonQueryMethods) => {
|
2023-01-09 03:41:14 -05:00
|
|
|
const findUserByUsername = async (username: string) =>
|
|
|
|
pool.maybeOne<User>(sql`
|
|
|
|
select ${sql.join(Object.values(fields), sql`,`)}
|
|
|
|
from ${table}
|
|
|
|
where ${fields.username}=${username}
|
|
|
|
`);
|
2023-01-04 02:39:27 -05:00
|
|
|
|
2023-01-09 03:41:14 -05:00
|
|
|
const findUserByEmail = async (email: string) =>
|
|
|
|
pool.maybeOne<User>(sql`
|
|
|
|
select ${sql.join(Object.values(fields), sql`,`)}
|
|
|
|
from ${table}
|
|
|
|
where lower(${fields.primaryEmail})=lower(${email})
|
|
|
|
`);
|
2021-07-04 04:41:46 -05:00
|
|
|
|
2023-01-09 03:41:14 -05:00
|
|
|
const findUserByPhone = async (phone: string) =>
|
|
|
|
pool.maybeOne<User>(sql`
|
2022-02-11 02:19:18 -05:00
|
|
|
select ${sql.join(Object.values(fields), sql`,`)}
|
|
|
|
from ${table}
|
2023-01-09 03:41:14 -05:00
|
|
|
where ${fields.primaryPhone}=${phone}
|
|
|
|
`);
|
|
|
|
|
2023-01-11 03:41:53 -05:00
|
|
|
const findUserById = async (id: string): Promise<User> =>
|
|
|
|
pool.one<User>(sql`
|
2023-01-09 03:41:14 -05:00
|
|
|
select ${sql.join(Object.values(fields), sql`,`)}
|
|
|
|
from ${table}
|
|
|
|
where ${fields.id}=${id}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const findUserByIdentity = async (target: string, userId: string) =>
|
|
|
|
pool.maybeOne<User>(
|
|
|
|
sql`
|
|
|
|
select ${sql.join(Object.values(fields), sql`,`)}
|
|
|
|
from ${table}
|
|
|
|
where ${fields.identities}::json#>>'{${sql.identifier([target])},userId}' = ${userId}
|
|
|
|
`
|
|
|
|
);
|
|
|
|
|
|
|
|
const hasUser = async (username: string, excludeUserId?: string) =>
|
|
|
|
pool.exists(sql`
|
|
|
|
select ${fields.id}
|
|
|
|
from ${table}
|
|
|
|
where ${fields.username}=${username}
|
|
|
|
${conditionalSql(excludeUserId, (id) => sql`and ${fields.id}<>${id}`)}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const hasUserWithId = async (id: string) =>
|
|
|
|
pool.exists(sql`
|
2022-02-11 02:19:18 -05:00
|
|
|
select ${fields.id}
|
|
|
|
from ${table}
|
2023-01-09 03:41:14 -05:00
|
|
|
where ${fields.id}=${id}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const hasUserWithEmail = async (email: string, excludeUserId?: string) =>
|
|
|
|
pool.exists(sql`
|
|
|
|
select ${fields.primaryEmail}
|
|
|
|
from ${table}
|
|
|
|
where lower(${fields.primaryEmail})=lower(${email})
|
|
|
|
${conditionalSql(excludeUserId, (id) => sql`and ${fields.id}<>${id}`)}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const hasUserWithPhone = async (phone: string, excludeUserId?: string) =>
|
|
|
|
pool.exists(sql`
|
|
|
|
select ${fields.primaryPhone}
|
|
|
|
from ${table}
|
|
|
|
where ${fields.primaryPhone}=${phone}
|
|
|
|
${conditionalSql(excludeUserId, (id) => sql`and ${fields.id}<>${id}`)}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const hasUserWithIdentity = async (target: string, userId: string) =>
|
|
|
|
pool.exists(
|
|
|
|
sql`
|
|
|
|
select ${fields.id}
|
|
|
|
from ${table}
|
|
|
|
where ${fields.identities}::json#>>'{${sql.identifier([target])},userId}' = ${userId}
|
|
|
|
`
|
|
|
|
);
|
|
|
|
|
2023-01-12 04:28:23 -05:00
|
|
|
const buildUserConditions = (search: Search, excludeUserIds: string[], userIds?: string[]) => {
|
2023-01-09 03:41:14 -05:00
|
|
|
const hasSearch = search.matches.length > 0;
|
|
|
|
const searchFields = [
|
|
|
|
Users.fields.id,
|
|
|
|
Users.fields.primaryEmail,
|
|
|
|
Users.fields.primaryPhone,
|
|
|
|
Users.fields.username,
|
|
|
|
Users.fields.name,
|
|
|
|
];
|
|
|
|
|
|
|
|
if (excludeUserIds.length > 0) {
|
|
|
|
// FIXME @sijie temp solution to filter out admin users,
|
|
|
|
// It is too complex to use join
|
|
|
|
return sql`
|
|
|
|
where ${fields.id} not in (${sql.join(excludeUserIds, sql`, `)})
|
|
|
|
${conditionalSql(
|
|
|
|
hasSearch,
|
|
|
|
() => sql`and (${buildConditionsFromSearch(search, searchFields)})`
|
|
|
|
)}
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2023-01-12 04:28:23 -05:00
|
|
|
if (userIds) {
|
|
|
|
return sql`
|
|
|
|
where ${fields.id} in (${userIds.length > 0 ? sql.join(userIds, sql`, `) : sql`null`})
|
|
|
|
${conditionalSql(
|
|
|
|
hasSearch,
|
|
|
|
() => sql`and (${buildConditionsFromSearch(search, searchFields)})`
|
|
|
|
)}
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2023-01-09 03:41:14 -05:00
|
|
|
return conditionalSql(
|
|
|
|
hasSearch,
|
|
|
|
() => sql`where ${buildConditionsFromSearch(search, searchFields)}`
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const defaultUserSearch = { matches: [], isCaseSensitive: false, joint: SearchJointMode.Or };
|
2022-06-21 21:22:15 -05:00
|
|
|
|
2023-01-12 04:28:23 -05:00
|
|
|
const countUsers = async (
|
|
|
|
search: Search = defaultUserSearch,
|
|
|
|
excludeUserIds: string[] = [],
|
|
|
|
userIds?: string[]
|
|
|
|
) =>
|
2023-01-09 03:41:14 -05:00
|
|
|
pool.one<{ count: number }>(sql`
|
|
|
|
select count(*)
|
2022-02-23 23:29:34 -05:00
|
|
|
from ${table}
|
2023-01-12 04:28:23 -05:00
|
|
|
${buildUserConditions(search, excludeUserIds, userIds)}
|
2023-01-09 03:41:14 -05:00
|
|
|
`);
|
|
|
|
|
|
|
|
const findUsers = async (
|
|
|
|
limit: number,
|
|
|
|
offset: number,
|
|
|
|
search: Search,
|
2023-01-12 04:28:23 -05:00
|
|
|
excludeUserIds: string[] = [],
|
|
|
|
userIds?: string[]
|
2023-01-09 03:41:14 -05:00
|
|
|
) =>
|
|
|
|
pool.any<User>(
|
|
|
|
sql`
|
|
|
|
select ${sql.join(
|
|
|
|
Object.values(fields).map((field) => sql`${table}.${field}`),
|
|
|
|
sql`,`
|
|
|
|
)}
|
|
|
|
from ${table}
|
2023-01-12 04:28:23 -05:00
|
|
|
${buildUserConditions(search, excludeUserIds, userIds)}
|
2023-01-29 02:30:05 -05:00
|
|
|
order by ${fields.createdAt} desc
|
2023-01-09 03:41:14 -05:00
|
|
|
limit ${limit}
|
|
|
|
offset ${offset}
|
|
|
|
`
|
|
|
|
);
|
|
|
|
|
|
|
|
const findUsersByIds = async (userIds: string[]) =>
|
2023-01-10 00:39:15 -05:00
|
|
|
userIds.length > 0
|
|
|
|
? pool.any<User>(sql`
|
|
|
|
select ${sql.join(Object.values(fields), sql`, `)}
|
|
|
|
from ${table}
|
|
|
|
where ${fields.id} in (${sql.join(userIds, sql`, `)})
|
|
|
|
`)
|
|
|
|
: [];
|
2023-01-09 03:41:14 -05:00
|
|
|
|
|
|
|
const updateUser = buildUpdateWhereWithPool(pool)<CreateUser, User>(Users, true);
|
|
|
|
|
|
|
|
const updateUserById = async (
|
|
|
|
id: string,
|
|
|
|
set: Partial<OmitAutoSetFields<CreateUser>>,
|
|
|
|
jsonbMode: 'replace' | 'merge' = 'merge'
|
|
|
|
) => updateUser({ set, where: { id }, jsonbMode });
|
|
|
|
|
|
|
|
const deleteUserById = async (id: string) => {
|
|
|
|
const { rowCount } = await pool.query(sql`
|
|
|
|
delete from ${table}
|
|
|
|
where ${fields.id}=${id}
|
|
|
|
`);
|
|
|
|
|
|
|
|
if (rowCount < 1) {
|
|
|
|
throw new DeletionError(Users.table, id);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const deleteUserIdentity = async (userId: string, target: string) =>
|
|
|
|
pool.one<User>(sql`
|
|
|
|
update ${table}
|
|
|
|
set ${fields.identities}=${fields.identities}::jsonb-${target}
|
|
|
|
where ${fields.id}=${userId}
|
|
|
|
returning *
|
|
|
|
`);
|
|
|
|
|
|
|
|
const hasActiveUsers = async () =>
|
|
|
|
pool.exists(sql`
|
|
|
|
select ${fields.id}
|
|
|
|
from ${table}
|
|
|
|
limit 1
|
|
|
|
`);
|
|
|
|
|
|
|
|
const getDailyNewUserCountsByTimeInterval = async (
|
|
|
|
startTimeExclusive: number,
|
|
|
|
endTimeInclusive: number
|
|
|
|
) =>
|
|
|
|
pool.any<{ date: string; count: number }>(sql`
|
|
|
|
select date(${fields.createdAt}), count(*)
|
|
|
|
from ${table}
|
|
|
|
where ${fields.createdAt} > to_timestamp(${startTimeExclusive}::double precision / 1000)
|
|
|
|
and ${fields.createdAt} <= to_timestamp(${endTimeInclusive}::double precision / 1000)
|
|
|
|
group by date(${fields.createdAt})
|
|
|
|
`);
|
|
|
|
|
|
|
|
return {
|
|
|
|
findUserByUsername,
|
|
|
|
findUserByEmail,
|
|
|
|
findUserByPhone,
|
|
|
|
findUserById,
|
|
|
|
findUserByIdentity,
|
|
|
|
hasUser,
|
|
|
|
hasUserWithId,
|
|
|
|
hasUserWithEmail,
|
|
|
|
hasUserWithPhone,
|
|
|
|
hasUserWithIdentity,
|
|
|
|
countUsers,
|
|
|
|
findUsers,
|
|
|
|
findUsersByIds,
|
|
|
|
updateUserById,
|
|
|
|
deleteUserById,
|
|
|
|
deleteUserIdentity,
|
|
|
|
hasActiveUsers,
|
|
|
|
getDailyNewUserCountsByTimeInterval,
|
|
|
|
};
|
2023-01-04 02:39:27 -05:00
|
|
|
};
|