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,133 +43,166 @@ const usersRoute = new Hono<HonoEnv>()
* - withMetadata: boolean * - withMetadata: boolean
*/ */
// Get all users with search
.get( .get(
"/", "/",
checkPermission("users.readAll"), checkPermission("users.readAll"),
requestValidator( requestValidator(
"query", "query",
z.object({ z.object({
includeTrashed: z includeTrashed: z
.string() .string()
.optional() .optional()
.transform((v) => v?.toLowerCase() === "true"), .transform((v) => v?.toLowerCase() === "true"),
withMetadata: z withMetadata: z
.string() .string()
.optional() .optional()
.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)
.where(
and(
includeTrashed ? undefined : isNull(users.deletedAt),
q
? or(
ilike(users.name, q),
ilike(users.username, q),
ilike(users.email, q),
eq(users.id, q)
)
: undefined
)
)
.offset(page * limit)
.limit(limit);
// Main query
const result = await db
.select({
id: users.id,
name: users.name,
email: users.email,
username: users.username,
isEnabled: users.isEnabled,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
...(includeTrashed ? { deletedAt: users.deletedAt } : {}),
company: respondents.companyName,
role: {
name: rolesSchema.name,
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(
.orderBy(users.createdAt); // sort by createdAt 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}%`), // Search by email
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;
// Group roles for each user to prevent duplication // Query to get unique user IDs based on pagination (Sub Query)
const userMap = new Map<string, { const userIdsQuery = db
id: string; .select({
name: string; id: users.id,
email: string | null; })
username: string; .from(users)
isEnabled: boolean; .leftJoin(respondents, eq(users.id, respondents.userId))
createdAt: Date; .leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId))
updatedAt: Date; .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id))
deletedAt?: Date; .where(
company: string | null; and(
roles: { id: string; name: string }[]; 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
)
)
.groupBy(users.id) // Group by user ID to avoid the effect of duplicate data
.offset(page * limit)
.limit(limit);
result.forEach((item) => { // Main Query
if (!userMap.has(item.id)) { const result = await db
userMap.set(item.id, { .select({
id: item.id, id: users.id,
name: item.name, name: users.name,
email: item.email ?? null, email: users.email,
username: item.username, username: users.username,
isEnabled: item.isEnabled ?? false, isEnabled: users.isEnabled,
createdAt: item.createdAt ?? new Date(), createdAt: users.createdAt,
updatedAt: item.updatedAt ?? new Date(), updatedAt: users.updatedAt,
deletedAt: item.deletedAt ?? undefined, ...(includeTrashed ? { deletedAt: users.deletedAt } : {}),
company: item.company, company: respondents.companyName,
roles: item.role ? [{ id: item.role.id, name: item.role.name }] : [], role: {
}); name: rolesSchema.name,
} else { id: rolesSchema.id,
const existingUser = userMap.get(item.id); },
if (item.role) { })
existingUser?.roles.push({ id: item.role.id, name: item.role.name }); .from(users)
} .leftJoin(respondents, eq(users.id, respondents.userId))
} .leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId))
}); .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id))
.where(inArray(users.id, userIdsQuery)) // Only take data based on IDs from subquery
.orderBy(users.createdAt);
// Return user data without duplication and roles in array form // Group roles for each user to avoid duplication
const groupedData = Array.from(userMap.values()); const userMap = new Map<
string,
{
id: string;
name: string;
email: string | null;
username: string;
isEnabled: boolean;
createdAt: Date;
updatedAt: Date;
deletedAt?: Date;
company: string | null;
roles: { id: string; name: string }[];
}
>();
return c.json({ result.forEach((item) => {
data: groupedData.map((d) => ({ ...d, fullCount: undefined })), if (!userMap.has(item.id)) {
_metadata: { userMap.set(item.id, {
currentPage: page, id: item.id,
totalPages: Math.ceil( name: item.name,
(Number(result[0]?.fullCount) ?? 0) / limit email: item.email ?? null,
), username: item.username,
totalItems: Number(result[0]?.fullCount) ?? 0, isEnabled: item.isEnabled ?? false,
perPage: limit, createdAt: item.createdAt ?? new Date(),
}, updatedAt: item.updatedAt ?? new Date(),
}); deletedAt: item.deletedAt ?? undefined,
company: item.company,
roles: item.role
? [{ id: item.role.id, name: item.role.name }]
: [],
});
} else {
const existingUser = userMap.get(item.id);
if (item.role) {
existingUser?.roles.push({
id: item.role.id,
name: item.role.name,
});
}
}
});
// Return user data without duplicates, with roles array
const groupedData = Array.from(userMap.values());
return c.json({
data: groupedData,
_metadata: {
currentPage: page,
totalPages: Math.ceil(totalCount / limit),
totalItems: totalCount,
perPage: limit,
},
});
} }
) )
//get user by id //get user by id
.get( .get(
@ -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>;
}, },
}), }),