Update: getAll in user API with search feature

This commit is contained in:
percyfikri 2024-09-18 11:13:25 +07:00
parent e5bc2b1a40
commit 196f327289
3 changed files with 152 additions and 127 deletions

View File

@ -43,6 +43,7 @@ const usersRoute = new Hono<HonoEnv>()
* - withMetadata: boolean * - withMetadata: boolean
*/ */
// Get all users with search
.get( .get(
"/", "/",
checkPermission("users.readAll"), checkPermission("users.readAll"),
@ -59,40 +60,68 @@ const usersRoute = new Hono<HonoEnv>()
.transform((v) => v?.toLowerCase() === "true"), .transform((v) => v?.toLowerCase() === "true"),
page: z.coerce.number().int().min(0).default(0), page: z.coerce.number().int().min(0).default(0),
limit: z.coerce.number().int().min(1).max(1000).default(10), limit: z.coerce.number().int().min(1).max(1000).default(10),
q: z.string().default(""), q: z.string().default(""), // Keyword search
}) })
), ),
async (c) => { async (c) => {
const { includeTrashed, page, limit, q } = c.req.valid("query"); const { includeTrashed, page, limit, q } = c.req.valid("query");
// Total count for pagination // Query to count total data without duplicates
const totalCountQuery = includeTrashed const totalCountQuery = db
? sql<number>`(SELECT count(*) FROM ${users})`
: sql<number>`(SELECT count(*) FROM ${users} WHERE ${users.deletedAt} IS NULL)`;
// Query to get unique user IDs with pagination (Sub Query)
const userIdsQuery = db
.select({ .select({
id: users.id, count: sql<number>`count(distinct ${users.id})`,
}) })
.from(users) .from(users)
.leftJoin(respondents, eq(users.id, respondents.userId))
.leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId))
.leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id))
.where( .where(
and( and(
includeTrashed ? undefined : isNull(users.deletedAt), includeTrashed ? undefined : isNull(users.deletedAt),
q q
? or( ? or(
ilike(users.name, q), ilike(users.name, `%${q}%`), // Search by name
ilike(users.username, q), ilike(users.username, `%${q}%`), // Search by username
ilike(users.email, q), ilike(users.email, `%${q}%`), // Search by email
eq(users.id, q) ilike(respondents.companyName, `%${q}%`), // Search by companyName (from respondents)
ilike(rolesSchema.name, `%${q}%`) // Search by role name (from rolesSchema)
)
: undefined
)
);
// Get the total count result from the query
const totalCountResult = await totalCountQuery;
const totalCount = totalCountResult[0]?.count || 0;
// Query to get unique user IDs based on pagination (Sub Query)
const userIdsQuery = db
.select({
id: users.id,
})
.from(users)
.leftJoin(respondents, eq(users.id, respondents.userId))
.leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId))
.leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id))
.where(
and(
includeTrashed ? undefined : isNull(users.deletedAt),
q
? or(
ilike(users.name, `%${q}%`), // Search by name
ilike(users.username, `%${q}%`), // Search by username
ilike(users.email, `%${q}%`),
ilike(respondents.companyName, `%${q}%`),
ilike(rolesSchema.name, `%${q}%`)
) )
: undefined : undefined
) )
) )
.groupBy(users.id) // Group by user ID to avoid the effect of duplicate data
.offset(page * limit) .offset(page * limit)
.limit(limit); .limit(limit);
// Main query // Main Query
const result = await db const result = await db
.select({ .select({
id: users.id, id: users.id,
@ -108,18 +137,18 @@ const usersRoute = new Hono<HonoEnv>()
name: rolesSchema.name, name: rolesSchema.name,
id: rolesSchema.id, id: rolesSchema.id,
}, },
fullCount: totalCountQuery,
}) })
.from(users) .from(users)
.leftJoin(respondents, eq(users.id, respondents.userId)) .leftJoin(respondents, eq(users.id, respondents.userId))
.leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId)) .leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId))
.leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id)) .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id))
.where(inArray(users.id, userIdsQuery)) // using ID from subquery .where(inArray(users.id, userIdsQuery)) // Only take data based on IDs from subquery
.orderBy(users.createdAt); // sort by createdAt .orderBy(users.createdAt);
// Group roles for each user to avoid duplication
// Group roles for each user to prevent duplication const userMap = new Map<
const userMap = new Map<string, { string,
{
id: string; id: string;
name: string; name: string;
email: string | null; email: string | null;
@ -130,7 +159,8 @@ const usersRoute = new Hono<HonoEnv>()
deletedAt?: Date; deletedAt?: Date;
company: string | null; company: string | null;
roles: { id: string; name: string }[]; roles: { id: string; name: string }[];
}>(); }
>();
result.forEach((item) => { result.forEach((item) => {
if (!userMap.has(item.id)) { if (!userMap.has(item.id)) {
@ -144,27 +174,30 @@ const usersRoute = new Hono<HonoEnv>()
updatedAt: item.updatedAt ?? new Date(), updatedAt: item.updatedAt ?? new Date(),
deletedAt: item.deletedAt ?? undefined, deletedAt: item.deletedAt ?? undefined,
company: item.company, company: item.company,
roles: item.role ? [{ id: item.role.id, name: item.role.name }] : [], roles: item.role
? [{ id: item.role.id, name: item.role.name }]
: [],
}); });
} else { } else {
const existingUser = userMap.get(item.id); const existingUser = userMap.get(item.id);
if (item.role) { if (item.role) {
existingUser?.roles.push({ id: item.role.id, name: item.role.name }); existingUser?.roles.push({
id: item.role.id,
name: item.role.name,
});
} }
} }
}); });
// Return user data without duplication and roles in array form // Return user data without duplicates, with roles array
const groupedData = Array.from(userMap.values()); const groupedData = Array.from(userMap.values());
return c.json({ return c.json({
data: groupedData.map((d) => ({ ...d, fullCount: undefined })), data: groupedData,
_metadata: { _metadata: {
currentPage: page, currentPage: page,
totalPages: Math.ceil( totalPages: Math.ceil(totalCount / limit),
(Number(result[0]?.fullCount) ?? 0) / limit totalItems: totalCount,
),
totalItems: Number(result[0]?.fullCount) ?? 0,
perPage: limit, perPage: limit,
}, },
}); });
@ -270,7 +303,7 @@ const usersRoute = new Hono<HonoEnv>()
.where(eq(respondents.phoneNumber, userData.phoneNumber)); .where(eq(respondents.phoneNumber, userData.phoneNumber));
if (existingUser.length > 0) { if (existingUser.length > 0) {
throw notFound({ throw forbidden({
message: "Email or username has been registered", message: "Email or username has been registered",
}) })
} }

View File

@ -216,14 +216,6 @@ export default function UserFormModal() {
disableAll: mutation.isPending, disableAll: mutation.isPending,
readonlyAll: formType === "detail", readonlyAll: formType === "detail",
inputs: [ inputs: [
{
type: "text",
label: "Id Pengguna",
readOnly: true,
variant: "filled",
...form.getInputProps("id"),
hidden: !form.values.id,
},
{ {
type: "text", type: "text",
label: "Nama", label: "Nama",
@ -293,7 +285,7 @@ export default function UserFormModal() {
onClick={() => navigate({ search: {} })} onClick={() => navigate({ search: {} })}
disabled={mutation.isPending} disabled={mutation.isPending}
> >
Close Tutup
</Button> </Button>
{formType !== "detail" && ( {formType !== "detail" && (
<Button <Button
@ -302,7 +294,7 @@ export default function UserFormModal() {
type="submit" type="submit"
loading={mutation.isPending} loading={mutation.isPending}
> >
Save Simpan
</Button> </Button>
)} )}
</Flex> </Flex>

View File

@ -54,7 +54,7 @@ export default function UsersPage() {
if (roles && roles.length > 0) { if (roles && roles.length > 0) {
return roles.map(role => role.name).join(", "); return roles.map(role => role.name).join(", ");
} }
return <div>Tidak ada peran yang diberikan</div>; return <div>-</div>;
}, },
}), }),