Update: getAll in user API with search feature
This commit is contained in:
parent
e5bc2b1a40
commit
196f327289
|
|
@ -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",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user