0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

Merge pull request #2858 from logto-io/gao-log-5112-core-query-factory-user

refactor(core): users and users roles query factories
This commit is contained in:
Gao Sun 2023-01-09 16:50:29 +08:00 committed by GitHub
commit 2964ff9588
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 294 additions and 225 deletions

View file

@ -2,9 +2,10 @@ import type { User, CreateUser, UserWithRoleNames } from '@logto/schemas';
import { SearchJointMode, Users } from '@logto/schemas';
import type { OmitAutoSetFields } from '@logto/shared';
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
import type { CommonQueryMethods } from 'slonik';
import { sql } from 'slonik';
import { buildUpdateWhere } from '#src/database/update-where.js';
import { buildUpdateWhereWithPool } from '#src/database/update-where.js';
import envSet from '#src/env-set/index.js';
import { DeletionError } from '#src/errors/SlonikError/index.js';
import type { Search } from '#src/utils/search.js';
@ -15,215 +16,261 @@ import { findUsersRolesByRoleId, findUsersRolesByUserId } from './users-roles.js
const { table, fields } = convertToIdentifiers(Users);
export const findUserByUsername = async (username: string) =>
envSet.pool.maybeOne<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.username}=${username}
`);
export const findUserByEmail = async (email: string) =>
envSet.pool.maybeOne<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where lower(${fields.primaryEmail})=lower(${email})
`);
export const findUserByPhone = async (phone: string) =>
envSet.pool.maybeOne<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.primaryPhone}=${phone}
`);
export const findUserById = async (id: string): Promise<UserWithRoleNames> => {
const user = await envSet.pool.one<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.id}=${id}
`);
const userRoles = await findUsersRolesByUserId(user.id);
const roles =
userRoles.length > 0 ? await findRolesByRoleIds(userRoles.map(({ roleId }) => roleId)) : [];
return {
...user,
roleNames: roles.map(({ name }) => name),
};
};
export const findUserByIdentity = async (target: string, userId: string) =>
envSet.pool.maybeOne<User>(
sql`
const createUserQueries = (pool: CommonQueryMethods) => {
const findUserByUsername = async (username: string) =>
pool.maybeOne<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.identities}::json#>>'{${sql.identifier([target])},userId}' = ${userId}
`
);
where ${fields.username}=${username}
`);
export const hasUser = async (username: string, excludeUserId?: string) =>
envSet.pool.exists(sql`
select ${fields.id}
from ${table}
where ${fields.username}=${username}
${conditionalSql(excludeUserId, (id) => sql`and ${fields.id}<>${id}`)}
`);
const findUserByEmail = async (email: string) =>
pool.maybeOne<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where lower(${fields.primaryEmail})=lower(${email})
`);
export const hasUserWithId = async (id: string) =>
envSet.pool.exists(sql`
select ${fields.id}
from ${table}
where ${fields.id}=${id}
`);
const findUserByPhone = async (phone: string) =>
pool.maybeOne<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.primaryPhone}=${phone}
`);
export const hasUserWithEmail = async (email: string, excludeUserId?: string) =>
envSet.pool.exists(sql`
select ${fields.primaryEmail}
from ${table}
where lower(${fields.primaryEmail})=lower(${email})
${conditionalSql(excludeUserId, (id) => sql`and ${fields.id}<>${id}`)}
`);
const findUserById = async (id: string): Promise<UserWithRoleNames> => {
const user = await pool.one<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.id}=${id}
`);
const userRoles = await findUsersRolesByUserId(user.id);
export const hasUserWithPhone = async (phone: string, excludeUserId?: string) =>
envSet.pool.exists(sql`
select ${fields.primaryPhone}
from ${table}
where ${fields.primaryPhone}=${phone}
${conditionalSql(excludeUserId, (id) => sql`and ${fields.id}<>${id}`)}
`);
const roles =
userRoles.length > 0 ? await findRolesByRoleIds(userRoles.map(({ roleId }) => roleId)) : [];
export const hasUserWithIdentity = async (target: string, userId: string) =>
envSet.pool.exists(
sql`
return {
...user,
roleNames: roles.map(({ name }) => name),
};
};
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.identities}::json#>>'{${sql.identifier([target])},userId}' = ${userId}
`
);
where ${fields.username}=${username}
${conditionalSql(excludeUserId, (id) => sql`and ${fields.id}<>${id}`)}
`);
const buildUserConditions = (search: Search, excludeUserIds: string[]) => {
const hasSearch = search.matches.length > 0;
const searchFields = [
Users.fields.id,
Users.fields.primaryEmail,
Users.fields.primaryPhone,
Users.fields.username,
Users.fields.name,
];
const hasUserWithId = async (id: string) =>
pool.exists(sql`
select ${fields.id}
from ${table}
where ${fields.id}=${id}
`);
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)})`
)}
`;
}
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}`)}
`);
return conditionalSql(
hasSearch,
() => sql`where ${buildConditionsFromSearch(search, searchFields)}`
);
};
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}`)}
`);
export const defaultUserSearch = { matches: [], isCaseSensitive: false, joint: SearchJointMode.Or };
const hasUserWithIdentity = async (target: string, userId: string) =>
pool.exists(
sql`
select ${fields.id}
from ${table}
where ${fields.identities}::json#>>'{${sql.identifier([target])},userId}' = ${userId}
`
);
export const countUsers = async (
search: Search = defaultUserSearch,
excludeUserIds: string[] = []
) =>
envSet.pool.one<{ count: number }>(sql`
select count(*)
from ${table}
${buildUserConditions(search, excludeUserIds)}
`);
const buildUserConditions = (search: Search, excludeUserIds: string[]) => {
const hasSearch = search.matches.length > 0;
const searchFields = [
Users.fields.id,
Users.fields.primaryEmail,
Users.fields.primaryPhone,
Users.fields.username,
Users.fields.name,
];
export const findUsers = async (
limit: number,
offset: number,
search: Search,
excludeUserIds: string[] = []
) =>
envSet.pool.any<User>(
sql`
select ${sql.join(
Object.values(fields).map((field) => sql`${table}.${field}`),
sql`,`
)}
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)})`
)}
`;
}
return conditionalSql(
hasSearch,
() => sql`where ${buildConditionsFromSearch(search, searchFields)}`
);
};
const defaultUserSearch = { matches: [], isCaseSensitive: false, joint: SearchJointMode.Or };
const countUsers = async (search: Search = defaultUserSearch, excludeUserIds: string[] = []) =>
pool.one<{ count: number }>(sql`
select count(*)
from ${table}
${buildUserConditions(search, excludeUserIds)}
limit ${limit}
offset ${offset}
`
);
`);
export const findUsersByIds = async (userIds: string[]) =>
userIds.length > 0
? envSet.pool.any<User>(sql`
const findUsers = async (
limit: number,
offset: number,
search: Search,
excludeUserIds: string[] = []
) =>
pool.any<User>(
sql`
select ${sql.join(
Object.values(fields).map((field) => sql`${table}.${field}`),
sql`,`
)}
from ${table}
${buildUserConditions(search, excludeUserIds)}
limit ${limit}
offset ${offset}
`
);
const findUsersByIds = async (userIds: string[]) =>
pool.any<User>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id} in (${sql.join(userIds, sql`, `)})
`)
: [];
`);
const updateUser = buildUpdateWhere<CreateUser, User>(Users, true);
const updateUser = buildUpdateWhereWithPool(pool)<CreateUser, User>(Users, true);
export const updateUserById = async (
id: string,
set: Partial<OmitAutoSetFields<CreateUser>>,
jsonbMode: 'replace' | 'merge' = 'merge'
) => updateUser({ set, where: { id }, jsonbMode });
const updateUserById = async (
id: string,
set: Partial<OmitAutoSetFields<CreateUser>>,
jsonbMode: 'replace' | 'merge' = 'merge'
) => updateUser({ set, where: { id }, jsonbMode });
export const deleteUserById = async (id: string) => {
const { rowCount } = await envSet.pool.query(sql`
delete from ${table}
where ${fields.id}=${id}
`);
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);
}
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})
`);
const findUsersByRoleName = async (roleName: string) => {
const role = await findRoleByRoleName(roleName);
if (!role) {
return [];
}
const usersRoles = await findUsersRolesByRoleId(role.id);
if (usersRoles.length === 0) {
return [];
}
return findUsersByIds(usersRoles.map(({ userId }) => userId));
};
return {
findUserByUsername,
findUserByEmail,
findUserByPhone,
findUserById,
findUserByIdentity,
hasUser,
hasUserWithId,
hasUserWithEmail,
hasUserWithPhone,
hasUserWithIdentity,
countUsers,
findUsers,
findUsersByIds,
updateUserById,
deleteUserById,
deleteUserIdentity,
hasActiveUsers,
getDailyNewUserCountsByTimeInterval,
findUsersByRoleName,
};
};
export const deleteUserIdentity = async (userId: string, target: string) =>
envSet.pool.one<User>(sql`
update ${table}
set ${fields.identities}=${fields.identities}::jsonb-${target}
where ${fields.id}=${userId}
returning *
`);
export const hasActiveUsers = async () =>
envSet.pool.exists(sql`
select ${fields.id}
from ${table}
limit 1
`);
export const getDailyNewUserCountsByTimeInterval = async (
startTimeExclusive: number,
endTimeInclusive: number
) =>
envSet.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})
`);
export const findUsersByRoleName = async (roleName: string) => {
const role = await findRoleByRoleName(roleName);
if (!role) {
return [];
}
const usersRoles = await findUsersRolesByRoleId(role.id);
return findUsersByIds(usersRoles.map(({ userId }) => userId));
};
/** @deprecated Will be removed soon. Use createUserQueries() factory instead. */
export const {
findUserByUsername,
findUserByEmail,
findUserByPhone,
findUserById,
findUserByIdentity,
hasUser,
hasUserWithId,
hasUserWithEmail,
hasUserWithPhone,
hasUserWithIdentity,
countUsers,
findUsers,
findUsersByIds,
updateUserById,
deleteUserById,
deleteUserIdentity,
hasActiveUsers,
getDailyNewUserCountsByTimeInterval,
findUsersByRoleName,
} = createUserQueries(envSet.pool);

View file

@ -1,57 +1,79 @@
import type { UsersRole } from '@logto/schemas';
import { UsersRoles } from '@logto/schemas';
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
import type { CommonQueryMethods } from 'slonik';
import { sql } from 'slonik';
import envSet from '#src/env-set/index.js';
const { table, fields } = convertToIdentifiers(UsersRoles);
export const findUsersRolesByUserId = async (userId: string) =>
envSet.pool.any<UsersRole>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.userId}=${userId}
`);
export const createUsersRolesQueries = (pool: CommonQueryMethods) => {
const countUsersRolesByRoleId = async (roleId: string) =>
pool.one<{ count: number }>(sql`
select count(*)
from ${table}
where ${fields.roleId}=${roleId}
`);
export const findUsersRolesByRoleId = async (roleId: string, limit?: number) =>
envSet.pool.any<UsersRole>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.roleId}=${roleId}
${conditionalSql(limit, (value) => sql`limit ${value}`)}
`);
const findFirstUsersRolesByRoleIdAndUserIds = async (roleId: string, userIds: string[]) =>
userIds.length > 0
? pool.maybeOne<UsersRole>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.roleId}=${roleId}
and ${fields.userId} in (${sql.join(userIds, sql`, `)})
limit 1
`)
: null;
export const countUsersRolesByRoleId = async (roleId: string) =>
envSet.pool.one<{ count: number }>(sql`
select count(*)
from ${table}
where ${fields.roleId}=${roleId}
`);
const findUsersRolesByUserId = async (userId: string) =>
pool.any<UsersRole>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.userId}=${userId}
`);
export const findFirstUsersRolesByRoleIdAndUserIds = async (roleId: string, userIds: string[]) =>
userIds.length > 0
? envSet.pool.maybeOne<UsersRole>(sql`
const findUsersRolesByRoleId = async (roleId: string, limit?: number) =>
pool.any<UsersRole>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.roleId}=${roleId}
and ${fields.userId} in (${sql.join(userIds, sql`, `)})
limit 1
`)
: null;
${conditionalSql(limit, (value) => sql`limit ${value}`)}
`);
export const insertUsersRoles = async (usersRoles: UsersRole[]) =>
envSet.pool.query(sql`
insert into ${table} (${fields.userId}, ${fields.roleId}) values
${sql.join(
usersRoles.map(({ userId, roleId }) => sql`(${userId}, ${roleId})`),
sql`, `
)}
`);
const insertUsersRoles = async (usersRoles: UsersRole[]) =>
pool.query(sql`
insert into ${table} (${fields.userId}, ${fields.roleId}) values
${sql.join(
usersRoles.map(({ userId, roleId }) => sql`(${userId}, ${roleId})`),
sql`, `
)}
`);
export const deleteUsersRolesByUserIdAndRoleId = async (userId: string, roleId: string) => {
await envSet.pool.query(sql`
delete from ${table}
where ${fields.userId} = ${userId} and ${fields.roleId} = ${roleId}
`);
const deleteUsersRolesByUserIdAndRoleId = async (userId: string, roleId: string) => {
await pool.query(sql`
delete from ${table}
where ${fields.userId} = ${userId} and ${fields.roleId} = ${roleId}
`);
};
return {
countUsersRolesByRoleId,
findFirstUsersRolesByRoleIdAndUserIds,
findUsersRolesByUserId,
findUsersRolesByRoleId,
insertUsersRoles,
deleteUsersRolesByUserIdAndRoleId,
};
};
/** @deprecated Will be removed soon. Use createUsersRolesQueries() factory instead. */
export const {
countUsersRolesByRoleId,
findFirstUsersRolesByRoleIdAndUserIds,
findUsersRolesByUserId,
findUsersRolesByRoleId,
insertUsersRoles,
deleteUsersRolesByUserIdAndRoleId,
} = createUsersRolesQueries(envSet.pool);