summaryrefslogtreecommitdiff
path: root/lib/roles/services.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-03-25 15:55:45 +0900
committerjoonhoekim <26rote@gmail.com>2025-03-25 15:55:45 +0900
commit1a2241c40e10193c5ff7008a7b7b36cc1d855d96 (patch)
tree8a5587f10ca55b162d7e3254cb088b323a34c41b /lib/roles/services.ts
initial commit
Diffstat (limited to 'lib/roles/services.ts')
-rw-r--r--lib/roles/services.ts300
1 files changed, 300 insertions, 0 deletions
diff --git a/lib/roles/services.ts b/lib/roles/services.ts
new file mode 100644
index 00000000..1a91d4fa
--- /dev/null
+++ b/lib/roles/services.ts
@@ -0,0 +1,300 @@
+"use server";
+
+import { revalidateTag, unstable_cache, unstable_noStore } from "next/cache";
+import db from "@/db/db";
+import { permissions, Role, rolePermissions, roles, RoleView, roleView, userRoles } from "@/db/schema/users";
+import { and, or, asc, desc, ilike, eq, inArray } from "drizzle-orm";
+import { filterColumns } from "@/lib/filter-columns";
+import {
+ selectRolesWithUserCount,
+ countRoles,
+ insertRole,
+ getRoleById,
+ updateRole,
+ deleteRolesByIds,
+ deleteUserRolesByIds,
+ findAllRoleView,
+} from "./repository";
+import { CreateRoleSchema, GetRolesSchema, UpdateRoleSchema } from "./validations";
+import { getErrorMessage } from "@/lib/handle-error";
+
+interface UpsertPermissionsInput {
+ roleIds: number[];
+ permissionKeys: string[];
+ itemTitle?: string;
+}
+
+export async function getRolesWithCount(input: GetRolesSchema) {
+ // unstable_cache: 특정 키와 함께 캐싱
+ return unstable_cache(
+ async () => {
+ try {
+ // 1) pagination
+ const offset = (input.page - 1) * input.perPage;
+
+ // 2) advanced filter
+ const advancedWhere = filterColumns({
+ table: roleView, // 또는 roleView
+ filters: input.filters,
+ joinOperator: input.joinOperator,
+ });
+
+ // 3) 글로벌 검색
+ let globalWhere;
+ if (input.search) {
+ const s = `%${input.search}%`;
+ // 예: roles.name 에 ilike 검색
+ globalWhere = or(ilike(roles.name, s));
+ }
+
+ // 4) 최종 where
+ const finalWhere = and(advancedWhere, globalWhere);
+
+ // (5) 정렬
+ const orderBy =
+ input.sort.length > 0
+ ? input.sort.map((item) =>
+ item.desc ? desc(roleView[item.id]) : asc(roleView[item.id])
+ )
+ : [desc(roleView.created_at)];
+
+
+ // 6) 트랜잭션 + Repository 호출
+ const { data, total } = await db.transaction(async (tx) => {
+ // 실제 SELECT
+ const data = await selectRolesWithUserCount(tx, {
+ where: finalWhere,
+ orderBy,
+ offset,
+ limit: input.perPage,
+ });
+
+ // 전체 개수
+ const total = await countRoles(tx, finalWhere);
+
+ return { data, total };
+ });
+
+ // 7) pageCount
+ const pageCount = Math.ceil(total / input.perPage);
+
+ return { data, pageCount };
+ } catch (err) {
+ // 에러시 기본값
+ return { data: [], pageCount: 0 };
+ }
+ },
+ [JSON.stringify(input)], // 캐싱 키
+ {
+ revalidate: 3600,
+ tags: ["roles"], // revalidateTag("roles")로 무효화
+ }
+ )();
+}
+
+export async function createRole(input: CreateRoleSchema) {
+ unstable_noStore(); // 캐싱 방지(Next.js 서버 액션용)
+ try {
+
+ await db.transaction(async (tx) => {
+ const [newRole] = await insertRole(tx, {
+ name: input.name,
+ domain: input.domain,
+ description: input.description ?? "",
+ companyId: input.domain === "partners" ? input.companyId ?? null : null,
+ });
+ });
+
+ revalidateTag("roles");
+
+ return { data: null, error: null };
+
+ } catch (err) {
+ return { data: null, error: getErrorMessage(err) };
+ }
+}
+
+
+export async function modifiRole(input: UpdateRoleSchema & { id: number }) {
+ unstable_noStore();
+
+ try {
+
+ const data = await db.transaction(async (tx) => {
+ // 1) 먼저 User 테이블 업데이트
+ const [res] = await updateRole(tx, input.id, {
+ name: input.name,
+ description: input.description,
+ domain: input.domain
+ });
+
+ return res;
+ });
+
+ // 3) 캐시 무효화
+ revalidateTag("roles");
+
+
+ return { data: null, error: null };
+ } catch (err) {
+ return { data: null, error: getErrorMessage(err) };
+ }
+}
+
+export async function removeRoles(input: { ids: number[] }) {
+ unstable_noStore();
+
+ try {
+ await db.transaction(async (tx) => {
+ // user_roles도 있으면 먼저 삭제해야 할 수 있음
+
+ await deleteUserRolesByIds(tx, input.ids);
+ await deleteRolesByIds(tx, input.ids);
+
+ });
+
+ revalidateTag("roles");
+ revalidateTag("user-role-counts");
+ revalidateTag("users");
+
+ return { data: null, error: null };
+ } catch (err) {
+ return { data: null, error: getErrorMessage(err) };
+ }
+}
+
+
+
+export async function assignRolesToUsers(roleIds: number[], userIds: number[]) {
+ // Next.js 서버 액션에서 캐싱 방지
+ unstable_noStore()
+
+ try {
+ await db.transaction(async (tx) => {
+ // 1) 기존 userRoles 삭제: userIds, roleIds에 해당하는 레코드만
+ await tx
+ .delete(userRoles)
+ .where(
+ and(
+ inArray(userRoles.roleId, roleIds),
+ inArray(userRoles.userId, userIds)
+ )
+ )
+
+ // 2) 새로 삽입
+ if (roleIds.length > 0 && userIds.length > 0) {
+ const newRows = []
+ for (const rid of roleIds) {
+ for (const uid of userIds) {
+ newRows.push({ roleId: rid, userId: uid })
+ }
+ }
+ await tx.insert(userRoles).values(newRows)
+ }
+ })
+
+ // 캐시 무효화
+ revalidateTag("users")
+ revalidateTag("roles")
+
+ return { data: null, error: null }
+ } catch (err) {
+ return { data: null, error: getErrorMessage(err) }
+ }
+}
+
+export async function getAllRoleView(domain?: "evcp" | "partners"): Promise<RoleView[]> {
+ try {
+ return await findAllRoleView(domain)
+ } catch (err) {
+ throw new Error("Failed to get roles")
+ }
+}
+
+export async function upsertPermissions(input: UpsertPermissionsInput) {
+ unstable_noStore();
+ try {
+ const { roleIds, permissionKeys, itemTitle } = input;
+ if (!roleIds.length || !permissionKeys.length) {
+ return; // nothing to do
+ }
+
+ const roleIdNums = roleIds
+
+ await db.transaction(async (tx) => {
+ for (const permKey of permissionKeys) {
+ // A) Check if permissionKey exists in "permissions" table
+ const [existingPerm] = await tx
+ .select({ id: permissions.id })
+ .from(permissions)
+ .where(eq(permissions.permissionKey, permKey))
+ .limit(1);
+
+ let permissionId: number;
+ if (!existingPerm) {
+ // Insert new permission
+ // description를 어떻게 만들지는 자유: itemTitle + permKey 등
+ const [inserted] = await tx
+ .insert(permissions)
+ .values({
+ permissionKey: permKey,
+ description: itemTitle ? `Menu: ${itemTitle} perm: ${permKey}` : permKey,
+ })
+ .returning({ id: permissions.id });
+
+ permissionId = inserted.id;
+ } else {
+ permissionId = existingPerm.id;
+ }
+
+ // B) now link (roleId, permissionId) in role_permissions
+ for (const rId of roleIdNums) {
+ // check if already exists
+ const [rp] = await tx
+ .select({ p: rolePermissions.permissionId })
+ .from(rolePermissions)
+ .where(and(eq(rolePermissions.roleId, rId), eq(rolePermissions.permissionId, permissionId)))
+ .limit(1);
+
+ if (!rp) {
+ // insert
+ await tx.insert(rolePermissions).values({
+ roleId: rId,
+ permissionId,
+ });
+ }
+ // if rp exists, skip
+ }
+ }
+ });
+
+ return { data: null, error: null };
+
+ } catch (err) {
+ return { data: null, error: getErrorMessage(err) };
+ }
+}
+
+
+export async function getMenuPermissions(
+ itemKey: string
+): Promise<{ roleId: number; permKey: string }[]> {
+ // itemKey = "alert-dialog"
+ // permKey = "alert-dialog.create", "alert-dialog.viewOwn", ...
+ const pattern = `${itemKey}.%`
+
+ // SELECT rp.role_id, p.permission_key
+ // FROM role_permissions rp
+ // JOIN permissions p ON p.id = rp.permissionId
+ // WHERE p.permission_key LIKE 'alert-dialog.%'
+ const rows = await db
+ .select({
+ roleId: rolePermissions.roleId,
+ permKey: permissions.permissionKey,
+ })
+ .from(rolePermissions)
+ .innerJoin(permissions, eq(permissions.id, rolePermissions.permissionId))
+ .where(ilike(permissions.permissionKey, pattern));
+
+ return rows;
+} \ No newline at end of file