From 6b2fda95aa39f6042f0b76334b2f0aa24fcdfe6e Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Thu, 4 Apr 2024 13:39:35 +0545 Subject: [PATCH 1/3] refactor(user): replace supertokens's roles with own roles --- .../src/model/roles/handlers/createRole.ts | 6 +- .../src/model/roles/handlers/deleteRole.ts | 4 +- .../model/roles/handlers/getPermissions.ts | 4 +- .../user/src/model/roles/handlers/getRoles.ts | 5 +- .../model/roles/handlers/updatePermissions.ts | 5 +- packages/user/src/model/roles/resolver.ts | 30 ++++---- packages/user/src/model/roles/service.ts | 75 ++++++++++++++----- packages/user/src/model/roles/sql.ts | 63 ++++++++++++++++ packages/user/src/model/roles/sqlFactory.ts | 47 ++++++++++++ packages/user/src/types/roles/index.ts | 6 ++ packages/user/src/types/roles/service.ts | 41 ++++++++++ packages/user/src/types/roles/sqlFactory.ts | 31 ++++++++ 12 files changed, 275 insertions(+), 42 deletions(-) create mode 100644 packages/user/src/model/roles/sql.ts create mode 100644 packages/user/src/model/roles/sqlFactory.ts create mode 100644 packages/user/src/types/roles/index.ts create mode 100644 packages/user/src/types/roles/service.ts create mode 100644 packages/user/src/types/roles/sqlFactory.ts diff --git a/packages/user/src/model/roles/handlers/createRole.ts b/packages/user/src/model/roles/handlers/createRole.ts index 78ccfe45b..5500797d4 100644 --- a/packages/user/src/model/roles/handlers/createRole.ts +++ b/packages/user/src/model/roles/handlers/createRole.ts @@ -5,7 +5,7 @@ import type { FastifyReply } from "fastify"; import type { SessionRequest } from "supertokens-node/framework/fastify"; const createRole = async (request: SessionRequest, reply: FastifyReply) => { - const { body, log } = request; + const { body, log, dbSchema, config, slonik } = request; const { role, permissions } = body as { role: string; @@ -13,9 +13,9 @@ const createRole = async (request: SessionRequest, reply: FastifyReply) => { }; try { - const service = new RoleService(); + const service = new RoleService(config, slonik, dbSchema); - const createResponse = await service.createRole(role, permissions); + const createResponse = await service.create({ role, permissions }); return reply.send(createResponse); } catch (error) { diff --git a/packages/user/src/model/roles/handlers/deleteRole.ts b/packages/user/src/model/roles/handlers/deleteRole.ts index 79a137523..f57ba64ae 100644 --- a/packages/user/src/model/roles/handlers/deleteRole.ts +++ b/packages/user/src/model/roles/handlers/deleteRole.ts @@ -5,7 +5,7 @@ import type { FastifyReply } from "fastify"; import type { SessionRequest } from "supertokens-node/framework/fastify"; const deleteRole = async (request: SessionRequest, reply: FastifyReply) => { - const { log, query } = request; + const { config, slonik, log, query, dbSchema } = request; try { let { role } = query as { role?: string }; @@ -25,7 +25,7 @@ const deleteRole = async (request: SessionRequest, reply: FastifyReply) => { }); } - const service = new RoleService(); + const service = new RoleService(config, slonik, dbSchema); const deleteResponse = await service.deleteRole(role); diff --git a/packages/user/src/model/roles/handlers/getPermissions.ts b/packages/user/src/model/roles/handlers/getPermissions.ts index dcda6ae2f..a98196dca 100644 --- a/packages/user/src/model/roles/handlers/getPermissions.ts +++ b/packages/user/src/model/roles/handlers/getPermissions.ts @@ -4,7 +4,7 @@ import type { FastifyReply } from "fastify"; import type { SessionRequest } from "supertokens-node/framework/fastify"; const getPermissions = async (request: SessionRequest, reply: FastifyReply) => { - const { log, query } = request; + const { config, dbSchema, slonik, log, query } = request; let permissions: string[] = []; try { @@ -21,7 +21,7 @@ const getPermissions = async (request: SessionRequest, reply: FastifyReply) => { return reply.send({ permissions }); } - const service = new RoleService(); + const service = new RoleService(config, slonik, dbSchema); permissions = await service.getPermissionsForRole(role); } diff --git a/packages/user/src/model/roles/handlers/getRoles.ts b/packages/user/src/model/roles/handlers/getRoles.ts index 7429c097d..612e8cd1b 100644 --- a/packages/user/src/model/roles/handlers/getRoles.ts +++ b/packages/user/src/model/roles/handlers/getRoles.ts @@ -4,10 +4,11 @@ import type { FastifyReply } from "fastify"; import type { SessionRequest } from "supertokens-node/framework/fastify"; const getRoles = async (request: SessionRequest, reply: FastifyReply) => { - const { log } = request; + const { config, dbSchema, log, slonik } = request; try { - const service = new RoleService(); + const service = new RoleService(config, slonik, dbSchema); + const roles = await service.getRoles(); return reply.send({ roles }); diff --git a/packages/user/src/model/roles/handlers/updatePermissions.ts b/packages/user/src/model/roles/handlers/updatePermissions.ts index 2bb89c5c9..6f5bc882b 100644 --- a/packages/user/src/model/roles/handlers/updatePermissions.ts +++ b/packages/user/src/model/roles/handlers/updatePermissions.ts @@ -8,7 +8,7 @@ const updatePermissions = async ( request: SessionRequest, reply: FastifyReply ) => { - const { log, body } = request; + const { body, config, dbSchema, log, slonik } = request; try { const { role, permissions } = body as { @@ -16,7 +16,8 @@ const updatePermissions = async ( permissions: string[]; }; - const service = new RoleService(); + const service = new RoleService(config, slonik, dbSchema); + const updatedPermissionsResponse = await service.updateRolePermissions( role, permissions diff --git a/packages/user/src/model/roles/resolver.ts b/packages/user/src/model/roles/resolver.ts index 491136be7..41d468431 100644 --- a/packages/user/src/model/roles/resolver.ts +++ b/packages/user/src/model/roles/resolver.ts @@ -14,15 +14,15 @@ const Mutation = { }, context: MercuriusContext ) => { - const { app } = context; + const { app, config, dbSchema, database } = context; try { - const service = new RoleService(); + const service = new RoleService(config, database, dbSchema); - const createResponse = await service.createRole( - arguments_.role, - arguments_.permissions - ); + const createResponse = await service.create({ + role: arguments_.role, + permissions: arguments_.permissions, + }); return createResponse; } catch (error) { @@ -53,10 +53,10 @@ const Mutation = { }, context: MercuriusContext ) => { - const { app } = context; + const { app, config, dbSchema, database } = context; try { - const service = new RoleService(); + const service = new RoleService(config, database, dbSchema); const { role } = arguments_; @@ -92,11 +92,12 @@ const Mutation = { }, context: MercuriusContext ) => { - const { app } = context; + const { app, config, database, dbSchema } = context; const { permissions, role } = arguments_; try { - const service = new RoleService(); + const service = new RoleService(config, database, dbSchema); + const updatedPermissionsResponse = await service.updateRolePermissions( role, permissions @@ -131,10 +132,11 @@ const Query = { arguments_: Record, context: MercuriusContext ) => { - const { app } = context; + const { app, config, database, dbSchema } = context; try { - const service = new RoleService(); + const service = new RoleService(config, database, dbSchema); + const roles = await service.getRoles(); return roles; @@ -157,13 +159,13 @@ const Query = { }, context: MercuriusContext ) => { - const { app } = context; + const { app, config, database, dbSchema } = context; const { role } = arguments_; let permissions: string[] = []; try { if (role) { - const service = new RoleService(); + const service = new RoleService(config, database, dbSchema); permissions = await service.getPermissionsForRole(role); } diff --git a/packages/user/src/model/roles/service.ts b/packages/user/src/model/roles/service.ts index 3b9de3ba1..422157f6b 100644 --- a/packages/user/src/model/roles/service.ts +++ b/packages/user/src/model/roles/service.ts @@ -1,28 +1,49 @@ +import { BaseService } from "@dzangolab/fastify-slonik"; import UserRoles from "supertokens-node/recipe/userroles"; +import RoleSqlFactory from "./sqlFactory"; import CustomApiError from "../../customApiError"; -class RoleService { - createRole = async ( - role: string, - permissions?: string[] - ): Promise<{ status: "OK" }> => { - const { roles } = await UserRoles.getAllRoles(role); - - if (roles.includes(role)) { - throw new CustomApiError({ - name: "ROLE_ALREADY_EXISTS", - message: "Unable to create role as it already exists", - statusCode: 422, +import type { Service } from "../../types/roles/service"; +import type { QueryResultRow } from "slonik"; + +class RoleService< + Role extends QueryResultRow, + RoleCreateInput extends QueryResultRow, + RoleUpdateInput extends QueryResultRow + > + extends BaseService + // eslint-disable-next-line prettier/prettier + implements Service { + create = async (data: RoleCreateInput) => { + const query = this.factory.getCreateSql({ + role: data.role, + } as unknown as RoleCreateInput); + + const result = (await this.database.connect(async (connection) => { + return connection.query(query).then((data) => { + return data.rows[0]; }); - } + })) as Role; - const createRoleResponse = await UserRoles.createNewRoleOrAddPermissions( - role, - permissions || [] + await this.addRolePermissions( + result.id as number, + data.permissions as string[] ); - return { status: createRoleResponse.status }; + return result; + }; + + addRolePermissions = async (roleId: number, permissions: string[]) => { + const query = this.factory.getAddRolePermissionSql(roleId, permissions); + + const result = (await this.database.connect(async (connection) => { + return connection.query(query).then((data) => { + return data.rows[0]; + }); + })) as Role; + + return result; }; deleteRole = async (role: string): Promise<{ status: "OK" }> => { @@ -118,6 +139,26 @@ class RoleService { permissions: permissionsResponse, }; }; + + get factory() { + if (!this.table) { + throw new Error(`Service table is not defined`); + } + + if (!this._factory) { + this._factory = new RoleSqlFactory< + Role, + RoleCreateInput, + RoleUpdateInput + >(this); + } + + return this._factory as RoleSqlFactory< + Role, + RoleCreateInput, + RoleUpdateInput + >; + } } export default RoleService; diff --git a/packages/user/src/model/roles/sql.ts b/packages/user/src/model/roles/sql.ts new file mode 100644 index 000000000..a6267abe5 --- /dev/null +++ b/packages/user/src/model/roles/sql.ts @@ -0,0 +1,63 @@ +import humps from "humps"; +import { sql } from "slonik"; + +import type { SortInput } from "@dzangolab/fastify-slonik"; +import type { IdentifierSqlToken } from "slonik"; + +const createSortFragment = ( + tableIdentifier: IdentifierSqlToken, + sort?: SortInput[] +) => { + if (sort && sort.length > 0) { + const arraySort = []; + + for (const data of sort) { + const direction = + data.direction === "ASC" ? sql.fragment`ASC` : sql.fragment`DESC`; + + let roleFragment; + + if (data.key === "roles") { + roleFragment = sql.fragment`user_role.role ->> 0`; + } + + const sortIdentifier = sql.identifier([ + ...tableIdentifier.names, + humps.decamelize(data.key), + ]); + + arraySort.push( + sql.fragment`${roleFragment ?? sortIdentifier} ${direction}` + ); + } + + return sql.fragment`ORDER BY ${sql.join(arraySort, sql.fragment`,`)}`; + } + + return sql.fragment``; +}; + +const createSortRoleFragment = ( + identifier: IdentifierSqlToken, + sort?: SortInput[] +) => { + let direction = sql.fragment`ASC`; + + if (!Array.isArray(sort)) { + sort = []; + } + + sort.some((sortItem) => { + if (sortItem.key === "roles" && sortItem.direction != "ASC") { + direction = sql.fragment`DESC`; + + return true; + } + + return false; + }); + + return sql.fragment`ORDER BY ${identifier} ${direction}`; +}; + +export { createSortFragment, createSortRoleFragment }; diff --git a/packages/user/src/model/roles/sqlFactory.ts b/packages/user/src/model/roles/sqlFactory.ts new file mode 100644 index 000000000..0e0c78a60 --- /dev/null +++ b/packages/user/src/model/roles/sqlFactory.ts @@ -0,0 +1,47 @@ +import { + DefaultSqlFactory, + createLimitFragment, + createFilterFragment, + createTableIdentifier, +} from "@dzangolab/fastify-slonik"; +import humps from "humps"; +import { QueryResultRow, QuerySqlToken, sql } from "slonik"; + +import { createSortFragment, createSortRoleFragment } from "./sql"; + +import type { SqlFactory } from "../../types/roles/sqlFactory"; +import type { FilterInput, SortInput } from "@dzangolab/fastify-slonik"; + +/* eslint-disable brace-style */ +class RoleSqlFactory< + Role extends QueryResultRow, + RoleCreateInput extends QueryResultRow, + RoleUpdateInput extends QueryResultRow + > + extends DefaultSqlFactory + implements SqlFactory +{ + /* eslint-enabled */ + + getAddRolePermissionSql = ( + roleId: number, + permissions: string[] + ): QuerySqlToken => { + const permissionsTable = createTableIdentifier( + "role_permissions", + this.schema + ); + + return sql.type(this.validationSchema)` + INSERT INTO ${permissionsTable} ("role_id", "permission") + SELECT * + FROM ${sql.unnest( + [permissions.map((permission) => [roleId, permission])], + ["integer", "varchar"] + )} + RETURNING *; + `; + }; +} + +export default RoleSqlFactory; diff --git a/packages/user/src/types/roles/index.ts b/packages/user/src/types/roles/index.ts new file mode 100644 index 000000000..ad0fcfa37 --- /dev/null +++ b/packages/user/src/types/roles/index.ts @@ -0,0 +1,6 @@ +interface RoleCreateInput { + role: string; + permissions: string[]; +} + +export type { RoleCreateInput }; diff --git a/packages/user/src/types/roles/service.ts b/packages/user/src/types/roles/service.ts new file mode 100644 index 000000000..58a06a594 --- /dev/null +++ b/packages/user/src/types/roles/service.ts @@ -0,0 +1,41 @@ +import type { ApiConfig } from "@dzangolab/fastify-config"; +import type { + Database, + FilterInput, + SortDirection, + SortInput, +} from "@dzangolab/fastify-slonik"; +import type { z } from "zod"; + +interface Service { + config: ApiConfig; + database: Database; + sortDirection: SortDirection; + sortKey: string; + schema: "public" | string; + table: string; + validationSchema: z.ZodTypeAny; + + all(fields: string[]): Promise>; + create(data: C): Promise; + delete(id: number | string): Promise; + findById(id: number | string): Promise; + getLimitDefault(): number; + getLimitMax(): number; + list( + limit?: number, + offset?: number, + filters?: FilterInput, + sort?: SortInput[] + ): Promise>; + count(filters?: FilterInput): Promise; + update(id: number | string, data: U): Promise; +} + +type PaginatedList = { + totalCount: number; + filteredCount: number; + data: readonly T[]; +}; + +export type { PaginatedList, Service }; diff --git a/packages/user/src/types/roles/sqlFactory.ts b/packages/user/src/types/roles/sqlFactory.ts new file mode 100644 index 000000000..bac4d0f0b --- /dev/null +++ b/packages/user/src/types/roles/sqlFactory.ts @@ -0,0 +1,31 @@ +import type { Service } from "./service"; +import type { ApiConfig } from "@dzangolab/fastify-config"; +import type { FilterInput, SortInput } from "@dzangolab/fastify-slonik"; +import type { FragmentSqlToken, QueryResultRow, QuerySqlToken } from "slonik"; + +interface SqlFactory< + T extends QueryResultRow, + C extends QueryResultRow, + U extends QueryResultRow +> { + config: ApiConfig; + service: Service; + + getAllSql(fields: string[], sort?: SortInput[]): QuerySqlToken; + getCreateSql(data: C): QuerySqlToken; + getDeleteSql(id: number | string): QuerySqlToken; + getFindByIdSql(id: number | string): QuerySqlToken; + getListSql( + limit: number, + offset?: number, + filters?: FilterInput, + sort?: SortInput[] + ): QuerySqlToken; + getSortInput(sort?: SortInput[]): SortInput[]; + getTableFragment(): FragmentSqlToken; + getUpdateSql(id: number | string, data: U): QuerySqlToken; + getCountSql(filters?: FilterInput): QuerySqlToken; + getAddRolePermissionSql(roleId: number, permission: string[]): QuerySqlToken; +} + +export type { SqlFactory }; From f70942ab951d9ecef13ed7eac7b350f61f103d6b Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Thu, 4 Apr 2024 18:03:18 +0545 Subject: [PATCH 2/3] refactor: add roles method getPermissionsForRole and getAllRolesWithPermissions --- packages/user/src/constants.ts | 8 ++ .../src/model/roles/handlers/deleteRole.ts | 2 +- .../model/roles/handlers/getPermissions.ts | 16 ++- .../user/src/model/roles/handlers/getRoles.ts | 2 +- packages/user/src/model/roles/resolver.ts | 13 ++- packages/user/src/model/roles/service.ts | 98 ++++++------------- packages/user/src/model/roles/sqlFactory.ts | 49 +++++++++- packages/user/src/types/roles/index.ts | 18 +++- packages/user/src/types/roles/service.ts | 6 ++ packages/user/src/types/roles/sqlFactory.ts | 2 + 10 files changed, 128 insertions(+), 86 deletions(-) diff --git a/packages/user/src/constants.ts b/packages/user/src/constants.ts index 2a0dd021c..192e7aa57 100644 --- a/packages/user/src/constants.ts +++ b/packages/user/src/constants.ts @@ -20,7 +20,12 @@ const ROUTE_ME = "/me"; const ROUTE_USERS = "/users"; const ROUTE_USERS_DISABLE = "/users/:id/disable"; const ROUTE_USERS_ENABLE = "/users/:id/enable"; + +// Tables +const TABLE_ROLES = "roles"; +const TABLE_ROLE_PERMISSIONS = "role_permissions"; const TABLE_USERS = "users"; +const TABLE_USER_ROLES = "user_roles"; // Roles const ROUTE_ROLES = "/roles"; @@ -75,4 +80,7 @@ export { ROUTE_USERS_ENABLE, TABLE_INVITATIONS, TABLE_USERS, + TABLE_ROLES, + TABLE_ROLE_PERMISSIONS, + TABLE_USER_ROLES, }; diff --git a/packages/user/src/model/roles/handlers/deleteRole.ts b/packages/user/src/model/roles/handlers/deleteRole.ts index f57ba64ae..b7f1b13c5 100644 --- a/packages/user/src/model/roles/handlers/deleteRole.ts +++ b/packages/user/src/model/roles/handlers/deleteRole.ts @@ -27,7 +27,7 @@ const deleteRole = async (request: SessionRequest, reply: FastifyReply) => { const service = new RoleService(config, slonik, dbSchema); - const deleteResponse = await service.deleteRole(role); + const deleteResponse = await service.delete(role); return reply.send(deleteResponse); } diff --git a/packages/user/src/model/roles/handlers/getPermissions.ts b/packages/user/src/model/roles/handlers/getPermissions.ts index a98196dca..456fc05e7 100644 --- a/packages/user/src/model/roles/handlers/getPermissions.ts +++ b/packages/user/src/model/roles/handlers/getPermissions.ts @@ -5,28 +5,26 @@ import type { SessionRequest } from "supertokens-node/framework/fastify"; const getPermissions = async (request: SessionRequest, reply: FastifyReply) => { const { config, dbSchema, slonik, log, query } = request; - let permissions: string[] = []; - try { - let { role } = query as { role?: string }; + let { role } = query as { role?: number }; if (role) { try { - role = JSON.parse(role) as string; + role = JSON.parse(role as unknown as string) as number; } catch { /* empty */ } - if (typeof role != "string") { - return reply.send({ permissions }); + if (typeof role != "number") { + throw new TypeError("Invalid input"); } const service = new RoleService(config, slonik, dbSchema); - permissions = await service.getPermissionsForRole(role); - } + const permissions = await service.getPermissionsForRole(role); - return reply.send({ permissions }); + return permissions; + } } catch (error) { log.error(error); reply.status(500); diff --git a/packages/user/src/model/roles/handlers/getRoles.ts b/packages/user/src/model/roles/handlers/getRoles.ts index 612e8cd1b..bbc232f62 100644 --- a/packages/user/src/model/roles/handlers/getRoles.ts +++ b/packages/user/src/model/roles/handlers/getRoles.ts @@ -9,7 +9,7 @@ const getRoles = async (request: SessionRequest, reply: FastifyReply) => { try { const service = new RoleService(config, slonik, dbSchema); - const roles = await service.getRoles(); + const roles = await service.getAllRolesWithPermissions(); return reply.send({ roles }); } catch (error) { diff --git a/packages/user/src/model/roles/resolver.ts b/packages/user/src/model/roles/resolver.ts index 41d468431..1a98d63d7 100644 --- a/packages/user/src/model/roles/resolver.ts +++ b/packages/user/src/model/roles/resolver.ts @@ -60,7 +60,7 @@ const Mutation = { const { role } = arguments_; - const deleteResponse = await service.deleteRole(role); + const deleteResponse = await service.delete(role); return deleteResponse; } catch (error) { @@ -137,7 +137,7 @@ const Query = { try { const service = new RoleService(config, database, dbSchema); - const roles = await service.getRoles(); + const roles = await service.getAllRolesWithPermissions(); return roles; } catch (error) { @@ -155,22 +155,21 @@ const Query = { rolePermissions: async ( parent: unknown, arguments_: { - role: string; + role: number; }, context: MercuriusContext ) => { const { app, config, database, dbSchema } = context; const { role } = arguments_; - let permissions: string[] = []; try { if (role) { const service = new RoleService(config, database, dbSchema); - permissions = await service.getPermissionsForRole(role); - } + const permissions = await service.getPermissionsForRole(role); - return permissions; + return permissions; + } } catch (error) { app.log.error(error); diff --git a/packages/user/src/model/roles/service.ts b/packages/user/src/model/roles/service.ts index 422157f6b..288d05f0d 100644 --- a/packages/user/src/model/roles/service.ts +++ b/packages/user/src/model/roles/service.ts @@ -2,8 +2,10 @@ import { BaseService } from "@dzangolab/fastify-slonik"; import UserRoles from "supertokens-node/recipe/userroles"; import RoleSqlFactory from "./sqlFactory"; +import { TABLE_ROLES } from "../../constants"; import CustomApiError from "../../customApiError"; +import type { RolePermission, RoleWithPermissions } from "../../types/roles"; import type { Service } from "../../types/roles/service"; import type { QueryResultRow } from "slonik"; @@ -15,6 +17,8 @@ class RoleService< extends BaseService // eslint-disable-next-line prettier/prettier implements Service { + static readonly TABLE = TABLE_ROLES; + create = async (data: RoleCreateInput) => { const query = this.factory.getCreateSql({ role: data.role, @@ -26,89 +30,52 @@ class RoleService< }); })) as Role; - await this.addRolePermissions( + const permissions = await this.addRolePermissions( result.id as number, data.permissions as string[] ); - return result; + return { + ...result, + permissions: permissions.map( + (permission: RolePermission) => permission.permission + ), + }; }; - addRolePermissions = async (roleId: number, permissions: string[]) => { - const query = this.factory.getAddRolePermissionSql(roleId, permissions); + addRolePermissions = async (id: number, permissions: string[]) => { + const query = this.factory.getAddRolePermissionSql(id, permissions); const result = (await this.database.connect(async (connection) => { return connection.query(query).then((data) => { - return data.rows[0]; + return data.rows; }); - })) as Role; + })) as RolePermission[]; return result; }; - deleteRole = async (role: string): Promise<{ status: "OK" }> => { - const response = await UserRoles.getUsersThatHaveRole(role); + getPermissionsForRole = async (id: number): Promise => { + const query = this.factory.getPermissionsForRoleSql(id); - if (response.status === "UNKNOWN_ROLE_ERROR") { - throw new CustomApiError({ - name: response.status, - message: `Invalid role`, - statusCode: 422, - }); - } + const result = await this.database.connect((connection) => { + return connection.any(query); + }); - if (response.users.length > 0) { - throw new CustomApiError({ - name: "ROLE_IN_USE", - message: - "The role is currently assigned to one or more users and cannot be deleted", - statusCode: 422, - }); - } - - const deleteRoleResponse = await UserRoles.deleteRole(role); - - return { status: deleteRoleResponse.status }; + return result as RolePermission[]; }; - getPermissionsForRole = async (role: string): Promise => { - let permissions: string[] = []; - - const response = await UserRoles.getPermissionsForRole(role); + getAllRolesWithPermissions = async (): Promise => { + const query = this.factory.getAllRolesWithPermissions(); - if (response.status === "OK") { - permissions = response.permissions; - } + const result = await this.database.connect((connection) => { + return connection.any(query); + }); - return permissions; + return result as RoleWithPermissions[]; }; - getRoles = async (): Promise<{ role: string; permissions: string[] }[]> => { - let roles: { role: string; permissions: string[] }[] = []; - - const response = await UserRoles.getAllRoles(); - - if (response.status === "OK") { - // [DU 2024-MAR-20] This is N+1 problem - roles = await Promise.all( - response.roles.map(async (role) => { - const response = await UserRoles.getPermissionsForRole(role); - - return { - role, - permissions: response.status === "OK" ? response.permissions : [], - }; - }) - ); - } - - return roles; - }; - - updateRolePermissions = async ( - role: string, - permissions: string[] - ): Promise<{ status: "OK"; permissions: string[] }> => { + updateRolePermissions = async (role: string, permissions: string[]) => { const response = await UserRoles.getPermissionsForRole(role); if (response.status === "UNKNOWN_ROLE_ERROR") { @@ -132,12 +99,11 @@ class RoleService< await UserRoles.removePermissionsFromRole(role, removedPermissions); await UserRoles.createNewRoleOrAddPermissions(role, newPermissions); - const permissionsResponse = await this.getPermissionsForRole(role); + const permissionsResponse = await this.getPermissionsForRole( + role as unknown as number + ); - return { - status: "OK", - permissions: permissionsResponse, - }; + return permissionsResponse; }; get factory() { diff --git a/packages/user/src/model/roles/sqlFactory.ts b/packages/user/src/model/roles/sqlFactory.ts index 0e0c78a60..55352eb46 100644 --- a/packages/user/src/model/roles/sqlFactory.ts +++ b/packages/user/src/model/roles/sqlFactory.ts @@ -6,9 +6,12 @@ import { } from "@dzangolab/fastify-slonik"; import humps from "humps"; import { QueryResultRow, QuerySqlToken, sql } from "slonik"; +import * as zod from "zod"; import { createSortFragment, createSortRoleFragment } from "./sql"; +import { TABLE_ROLE_PERMISSIONS } from "../../constants"; +import type { Service } from "../../types/roles/service"; import type { SqlFactory } from "../../types/roles/sqlFactory"; import type { FilterInput, SortInput } from "@dzangolab/fastify-slonik"; @@ -22,13 +25,14 @@ class RoleSqlFactory< implements SqlFactory { /* eslint-enabled */ + protected declare _service: Service; getAddRolePermissionSql = ( roleId: number, permissions: string[] ): QuerySqlToken => { const permissionsTable = createTableIdentifier( - "role_permissions", + TABLE_ROLE_PERMISSIONS, this.schema ); @@ -42,6 +46,49 @@ class RoleSqlFactory< RETURNING *; `; }; + + getPermissionsForRoleSql = (id: number): QuerySqlToken => { + const permissionsIdentifier = createTableIdentifier( + TABLE_ROLE_PERMISSIONS, + this.schema + ); + + const filters: FilterInput = { + key: "rowId", + operator: "eq", + value: `${id}`, + }; + + return sql.unsafe` + SELECT * + FROM ${permissionsIdentifier} + ${createFilterFragment(filters, permissionsIdentifier)}; + + `; + }; + + getAllRolesWithPermissions = (): QuerySqlToken => { + const rolePermissionsIdentifier = createTableIdentifier( + TABLE_ROLE_PERMISSIONS, + this.schema + ); + + return sql.unsafe` + SELECT + ${this.getTableFragment()}.*, + COALESCE(user_permissions.permission, '[]') AS permissions + FROM ${this.getTableFragment()} + LEFT JOIN LATERAL ( + SELECT jsonb_agg(rp.permission) AS permissions + FROM ${rolePermissionsIdentifier} as rp + WHERE rp.role_id = ${this.getTableFragment()}.id + ) AS role_permissions ON TRUE + `; + }; + + get service() { + return this._service; + } } export default RoleSqlFactory; diff --git a/packages/user/src/types/roles/index.ts b/packages/user/src/types/roles/index.ts index ad0fcfa37..90fa78117 100644 --- a/packages/user/src/types/roles/index.ts +++ b/packages/user/src/types/roles/index.ts @@ -1,6 +1,22 @@ +interface Role { + id: number; + role: string; + default: string; +} + +interface RoleWithPermissions extends Role { + permissions: string[]; +} + interface RoleCreateInput { role: string; permissions: string[]; } -export type { RoleCreateInput }; +interface RolePermission { + id: string; + roleId: number; + permission: string; +} + +export type { Role, RoleCreateInput, RolePermission, RoleWithPermissions }; diff --git a/packages/user/src/types/roles/service.ts b/packages/user/src/types/roles/service.ts index 58a06a594..bd4167eeb 100644 --- a/packages/user/src/types/roles/service.ts +++ b/packages/user/src/types/roles/service.ts @@ -1,3 +1,4 @@ +import type { Role, RolePermission, RoleWithPermissions } from "."; import type { ApiConfig } from "@dzangolab/fastify-config"; import type { Database, @@ -30,6 +31,11 @@ interface Service { ): Promise>; count(filters?: FilterInput): Promise; update(id: number | string, data: U): Promise; + addRolePermissions( + id: number, + permission: string[] + ): Promise; + getPermissionsForRole(id: number): Promise; } type PaginatedList = { diff --git a/packages/user/src/types/roles/sqlFactory.ts b/packages/user/src/types/roles/sqlFactory.ts index bac4d0f0b..16c7e5673 100644 --- a/packages/user/src/types/roles/sqlFactory.ts +++ b/packages/user/src/types/roles/sqlFactory.ts @@ -26,6 +26,8 @@ interface SqlFactory< getUpdateSql(id: number | string, data: U): QuerySqlToken; getCountSql(filters?: FilterInput): QuerySqlToken; getAddRolePermissionSql(roleId: number, permission: string[]): QuerySqlToken; + getPermissionsForRoleSql(id: number): QuerySqlToken; + getAllRolesWithPermissions(): QuerySqlToken; } export type { SqlFactory }; From 7763b1867b02a3932c521b01929d493dea8572c0 Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Fri, 5 Apr 2024 09:35:24 +0545 Subject: [PATCH 3/3] refactor(user): update updateRolePermissions method of roles service --- .../user/src/model/roles/handlers/getRoles.ts | 2 +- .../model/roles/handlers/updatePermissions.ts | 6 +-- packages/user/src/model/roles/resolver.ts | 8 +-- packages/user/src/model/roles/service.ts | 36 ++++++++------ packages/user/src/model/roles/sqlFactory.ts | 49 ++++++++++++++++++- packages/user/src/types/roles/service.ts | 9 ++++ packages/user/src/types/roles/sqlFactory.ts | 3 +- 7 files changed, 89 insertions(+), 24 deletions(-) diff --git a/packages/user/src/model/roles/handlers/getRoles.ts b/packages/user/src/model/roles/handlers/getRoles.ts index bbc232f62..612e8cd1b 100644 --- a/packages/user/src/model/roles/handlers/getRoles.ts +++ b/packages/user/src/model/roles/handlers/getRoles.ts @@ -9,7 +9,7 @@ const getRoles = async (request: SessionRequest, reply: FastifyReply) => { try { const service = new RoleService(config, slonik, dbSchema); - const roles = await service.getAllRolesWithPermissions(); + const roles = await service.getRoles(); return reply.send({ roles }); } catch (error) { diff --git a/packages/user/src/model/roles/handlers/updatePermissions.ts b/packages/user/src/model/roles/handlers/updatePermissions.ts index 6f5bc882b..0ae8d5172 100644 --- a/packages/user/src/model/roles/handlers/updatePermissions.ts +++ b/packages/user/src/model/roles/handlers/updatePermissions.ts @@ -11,15 +11,15 @@ const updatePermissions = async ( const { body, config, dbSchema, log, slonik } = request; try { - const { role, permissions } = body as { - role: string; + const { roleId, permissions } = body as { + roleId: number; permissions: string[]; }; const service = new RoleService(config, slonik, dbSchema); const updatedPermissionsResponse = await service.updateRolePermissions( - role, + roleId, permissions ); diff --git a/packages/user/src/model/roles/resolver.ts b/packages/user/src/model/roles/resolver.ts index 1a98d63d7..ef2640168 100644 --- a/packages/user/src/model/roles/resolver.ts +++ b/packages/user/src/model/roles/resolver.ts @@ -87,19 +87,19 @@ const Mutation = { updateRolePermissions: async ( parent: unknown, arguments_: { - role: string; + roleId: number; permissions: string[]; }, context: MercuriusContext ) => { const { app, config, database, dbSchema } = context; - const { permissions, role } = arguments_; + const { permissions, roleId } = arguments_; try { const service = new RoleService(config, database, dbSchema); const updatedPermissionsResponse = await service.updateRolePermissions( - role, + roleId, permissions ); @@ -137,7 +137,7 @@ const Query = { try { const service = new RoleService(config, database, dbSchema); - const roles = await service.getAllRolesWithPermissions(); + const roles = await service.getRoles(); return roles; } catch (error) { diff --git a/packages/user/src/model/roles/service.ts b/packages/user/src/model/roles/service.ts index 288d05f0d..c0cd5663a 100644 --- a/packages/user/src/model/roles/service.ts +++ b/packages/user/src/model/roles/service.ts @@ -1,5 +1,4 @@ import { BaseService } from "@dzangolab/fastify-slonik"; -import UserRoles from "supertokens-node/recipe/userroles"; import RoleSqlFactory from "./sqlFactory"; import { TABLE_ROLES } from "../../constants"; @@ -22,6 +21,7 @@ class RoleService< create = async (data: RoleCreateInput) => { const query = this.factory.getCreateSql({ role: data.role, + default: data.default, } as unknown as RoleCreateInput); const result = (await this.database.connect(async (connection) => { @@ -43,8 +43,8 @@ class RoleService< }; }; - addRolePermissions = async (id: number, permissions: string[]) => { - const query = this.factory.getAddRolePermissionSql(id, permissions); + addRolePermissions = async (roleId: number, permissions: string[]) => { + const query = this.factory.getAddRolePermissionSql(roleId, permissions); const result = (await this.database.connect(async (connection) => { return connection.query(query).then((data) => { @@ -65,8 +65,8 @@ class RoleService< return result as RolePermission[]; }; - getAllRolesWithPermissions = async (): Promise => { - const query = this.factory.getAllRolesWithPermissions(); + getRoles = async (): Promise => { + const query = this.factory.getRolesSql(); const result = await this.database.connect((connection) => { return connection.any(query); @@ -75,10 +75,20 @@ class RoleService< return result as RoleWithPermissions[]; }; - updateRolePermissions = async (role: string, permissions: string[]) => { - const response = await UserRoles.getPermissionsForRole(role); + removePermissionsFromRole = async (roleId: number, permissions: string[]) => { + const query = this.factory.getRemovePermissionsSql(roleId, permissions); - if (response.status === "UNKNOWN_ROLE_ERROR") { + const result = await this.database.connect((connection) => { + return connection.any(query); + }); + + return result; + }; + + updateRolePermissions = async (roleId: number, permissions: string[]) => { + const response = await this.findById(roleId); + + if (!response) { throw new CustomApiError({ name: "UNKNOWN_ROLE_ERROR", message: `Invalid role`, @@ -86,7 +96,7 @@ class RoleService< }); } - const rolePermissions = response.permissions; + const rolePermissions = response.permissions as string[]; const newPermissions = permissions.filter( (permission) => !rolePermissions.includes(permission) @@ -96,12 +106,10 @@ class RoleService< (permission) => !permissions.includes(permission) ); - await UserRoles.removePermissionsFromRole(role, removedPermissions); - await UserRoles.createNewRoleOrAddPermissions(role, newPermissions); + await this.removePermissionsFromRole(roleId, removedPermissions); + await this.addRolePermissions(roleId, newPermissions); - const permissionsResponse = await this.getPermissionsForRole( - role as unknown as number - ); + const permissionsResponse = await this.getPermissionsForRole(roleId); return permissionsResponse; }; diff --git a/packages/user/src/model/roles/sqlFactory.ts b/packages/user/src/model/roles/sqlFactory.ts index 55352eb46..4dbcc4b3a 100644 --- a/packages/user/src/model/roles/sqlFactory.ts +++ b/packages/user/src/model/roles/sqlFactory.ts @@ -67,7 +67,7 @@ class RoleSqlFactory< `; }; - getAllRolesWithPermissions = (): QuerySqlToken => { + getRolesSql = (): QuerySqlToken => { const rolePermissionsIdentifier = createTableIdentifier( TABLE_ROLE_PERMISSIONS, this.schema @@ -86,6 +86,53 @@ class RoleSqlFactory< `; }; + getFindByIdSql = (id: number | string): QuerySqlToken => { + const rolePermissionsIdentifier = createTableIdentifier( + TABLE_ROLE_PERMISSIONS, + this.schema + ); + + return sql.unsafe` + SELECT + ${this.getTableFragment()}.*, + COALESCE(user_permissions.permission, '[]') AS permissions + FROM ${this.getTableFragment()} + LEFT JOIN LATERAL ( + SELECT jsonb_agg(rp.permission) AS permissions + FROM ${rolePermissionsIdentifier} as rp + WHERE rp.role_id = ${this.getTableFragment()}.id + ) AS role_permissions ON TRUE + WHERE id = ${id} + `; + }; + + getRemovePermissionsSql = ( + roleId: number, + permissions: string[] + ): QuerySqlToken => { + const rolePermissionsIdentifier = createTableIdentifier( + TABLE_ROLE_PERMISSIONS, + this.schema + ); + + const filters: FilterInput = { + OR: permissions.map((permission) => { + return { + AND: [ + { key: "roleId", operator: "eq", value: roleId }, + { key: "permission", operator: "eq", value: permission }, + ], + }; + }), + } as FilterInput; + + return sql.unsafe` + DELETE FROM ${rolePermissionsIdentifier} + ${createFilterFragment(filters, rolePermissionsIdentifier)} + RETURNING *; + `; + }; + get service() { return this._service; } diff --git a/packages/user/src/types/roles/service.ts b/packages/user/src/types/roles/service.ts index bd4167eeb..401204ae8 100644 --- a/packages/user/src/types/roles/service.ts +++ b/packages/user/src/types/roles/service.ts @@ -36,6 +36,15 @@ interface Service { permission: string[] ): Promise; getPermissionsForRole(id: number): Promise; + getRoles(): Promise; + updateRolePermissions( + roleId: number, + permission: string[] + ): Promise; + removePermissionsFromRole( + roleId: number, + permission: string[] + ): Promise; } type PaginatedList = { diff --git a/packages/user/src/types/roles/sqlFactory.ts b/packages/user/src/types/roles/sqlFactory.ts index 16c7e5673..2b1af6ff4 100644 --- a/packages/user/src/types/roles/sqlFactory.ts +++ b/packages/user/src/types/roles/sqlFactory.ts @@ -27,7 +27,8 @@ interface SqlFactory< getCountSql(filters?: FilterInput): QuerySqlToken; getAddRolePermissionSql(roleId: number, permission: string[]): QuerySqlToken; getPermissionsForRoleSql(id: number): QuerySqlToken; - getAllRolesWithPermissions(): QuerySqlToken; + getRolesSql(): QuerySqlToken; + getRemovePermissionsSql(roleId: number, permissions: string[]): QuerySqlToken; } export type { SqlFactory };