Skip to content

Commit

Permalink
Topic /k1ch/ Introduce API POST: clients/{client_id}/permissions (#129
Browse files Browse the repository at this point in the history
)

* feat: k1ch/ introduce POST:/clients/{client_id}/permissions

* chore: k1ch / modify pgErrorHandler to return Error object

* chore: k1ch / add tests for admin-permission DB layer

* chore: k1ch / add tests for POST:clients/{client_id}/permissions

* refactor: k1ch / throw Error instead of object
  • Loading branch information
k1ch authored Nov 1, 2024
1 parent 63dd5c4 commit ae1665d
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 68 deletions.
39 changes: 38 additions & 1 deletion database/layer/admin-permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,53 @@ const getPermissionsByRoleKey = async (roleKey) => {
return await usherDb('permissions')
.join('rolepermissions', 'permissions.key', '=', 'rolepermissions.permissionkey')
.where({ 'rolepermissions.rolekey': roleKey })
.select('permissions.*');
.select('permissions.*')
} catch (err) {
throw pgErrorHandler(err)
}
}

/**
* Insert a new permission
*
* @param {Object} permissionObject - The data for the new permission
* @param {string} permissionObject.name - The name of permission
* @param {number} permissionObject.clientkey - A valid client key
* @param {string} permissionObject.description - A description of the permission
* @returns {Promise<Object>} - A promise that resolves to the inserted permission object
*/
const insertPermission = async (permissionObject) => {
try {
const [permission] = await usherDb('permissions').insert(permissionObject).returning('*')
return permission
} catch (err) {
throw pgErrorHandler(err);
}
}

/**
* Get permissions by name and clientKey
*
* @param {string} name - The name of the permission
* @param {number} clientKey - The client key
* @returns {Promise<Array<Object>>} - A promise that resolves to an array of permissions
*/
const getPermissionsByNameClientKey = async (name, clientKey) => {
try {
const permissions = await usherDb('permissions')
.where({ name, clientkey: clientKey })
return permissions
} catch (err) {
throw pgErrorHandler(err)
}
}

module.exports = {
insertPermissionByClientId,
updatePermissionByPermissionname,
deletePermissionByPermissionname,
getPermission,
getPermissionsByRoleKey,
insertPermission,
getPermissionsByNameClientKey,
}
60 changes: 59 additions & 1 deletion database/test/db-admin-permissions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ const adminPermissions = require('../layer/admin-permission')
const { usherDb } = require('../layer/knex')

describe('Admin permissions view', () => {
let validClientKey
let permissionTableColumns

before(async () => {
const { key: clientKey } = await usherDb('clients').select('key').first()
validClientKey = clientKey
permissionTableColumns = Object.keys(await usherDb('permissions').columnInfo())
})

describe('Test Get permission', () => {
let validPermissionKey

Expand All @@ -28,9 +37,58 @@ describe('Admin permissions view', () => {
.count('permissionkey as permission_count')
.groupBy('rolekey')
.orderBy('permission_count', 'desc')
.first();
.first()
const permissions = await adminPermissions.getPermissionsByRoleKey(rolekey)
assert.equal(permission_count, permissions.length)
})
})

describe('Insert Permission', () => {
it('Should insert a new permission successfully', async () => {
const permissionObject = {
name: 'Test Permission',
clientkey: validClientKey,
description: 'A test permission',
}
const insertedPermission = await adminPermissions.insertPermission(permissionObject)
assert.equal(insertedPermission.name, permissionObject.name)
assert.equal(insertedPermission.clientkey, permissionObject.clientkey)
assert.equal(insertedPermission.description, permissionObject.description)
assert.ok(permissionTableColumns.every((col) => col in insertedPermission))
await usherDb('permissions').where({ key: insertedPermission.key }).del()
})

it('Should throw an error when inserting a permission with invalid clientkey', async () => {
const invalidPermissionObject = {
name: 'Invalid Test Permission',
clientkey: null,
description: 'This should fail',
}
try {
await adminPermissions.insertPermission(invalidPermissionObject)
assert.fail('Expected an error but did not get one')
} catch (err) {
assert.ok(err instanceof Error)
}
})
})

describe('Get Permissions by Name and Client Key', () => {
it('Should return permissions for a given name and clientkey', async () => {
const permission = await usherDb('permissions').select('*').first()
const permissions = await adminPermissions.getPermissionsByNameClientKey(
permission.name,
permission.clientkey
)
assert.ok(permissions.length > 0)
assert.equal(permissions[0].name, permission.name)
assert.equal(permissions[0].clientkey, permission.clientkey)
assert.ok(permissionTableColumns.every((col) => col in permissions[0]))
})

it('Should return an empty array if no permissions match the criteria', async () => {
const permissions = await adminPermissions.getPermissionsByNameClientKey('Nonexistent Name', 99999)
assert.deepEqual(permissions, [])
})
})
})
62 changes: 32 additions & 30 deletions database/utils/pgErrorHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,79 +6,81 @@ const { PgErrorCodes } = require('../constant/PgErrorCodes')
* @returns {message: text, httpStatusCode: number}
*/
const pgErrorHandler = (pgDbError) => {
const error = {}
let errorMessage;
let httpStatusCode;
switch (pgDbError.code) {
case PgErrorCodes.UniqueViolation:
error.message = 'The operation would result in duplicate resources!'
error.httpStatusCode = 409
errorMessage = 'The operation would result in duplicate resources!'
httpStatusCode = 409
break

case PgErrorCodes.CheckViolation:
error.message = 'The operation would violate a check constraint!'
error.httpStatusCode = 400
errorMessage = 'The operation would violate a check constraint!'
httpStatusCode = 400
break

case PgErrorCodes.NotNullViolation:
error.message = 'A required value is missing!'
error.httpStatusCode = 400
errorMessage = 'A required value is missing!'
httpStatusCode = 400
break

case PgErrorCodes.ForeignKeyViolation:
error.message = 'Referenced resource is invalid!'
error.httpStatusCode = 400
errorMessage = 'Referenced resource is invalid!'
httpStatusCode = 400
break

case PgErrorCodes.InvalidTextRepresentation:
error.message = 'The provided data format is invalid!'
error.httpStatusCode = 400
errorMessage = 'The provided data format is invalid!'
httpStatusCode = 400
break

case PgErrorCodes.UndefinedColumn:
error.message = 'Internal DB Error: Bad query - Specified column is invalid!'
error.httpStatusCode = 500
errorMessage = 'Internal DB Error: Bad query - Specified column is invalid!'
httpStatusCode = 500
break

case PgErrorCodes.SerializationFailure:
error.message = 'Internal DB Error: A transaction serialization error occurred!'
error.httpStatusCode = 500
errorMessage = 'Internal DB Error: A transaction serialization error occurred!'
httpStatusCode = 500
break

case PgErrorCodes.DeadlockDetected:
error.message = 'Internal DB Error: The operation was halted due to a potential deadlock!'
error.httpStatusCode = 500
errorMessage = 'Internal DB Error: The operation was halted due to a potential deadlock!'
httpStatusCode = 500
break

case PgErrorCodes.SyntaxError:
error.message = 'Internal DB Error: There is a syntax error in the provided SQL or data!'
error.httpStatusCode = 500
errorMessage = 'Internal DB Error: There is a syntax error in the provided SQL or data!'
httpStatusCode = 500
break

case PgErrorCodes.UndefinedTable:
error.message = 'Internal DB Error: The table or view you are trying to access does not exist!'
error.httpStatusCode = 500
errorMessage = 'Internal DB Error: The table or view you are trying to access does not exist!'
httpStatusCode = 500
break

case PgErrorCodes.DiskFull:
error.message = 'Internal DB Error: The operation failed due to insufficient disk space!'
error.httpStatusCode = 500
errorMessage = 'Internal DB Error: The operation failed due to insufficient disk space!'
httpStatusCode = 500
break

case PgErrorCodes.OutOfMemory:
error.message = 'Internal DB Error: The system ran out of memory!'
error.httpStatusCode = 500
errorMessage = 'Internal DB Error: The system ran out of memory!'
httpStatusCode = 500
break

case PgErrorCodes.TooManyConnections:
error.message = 'Internal DB Error: There are too many connections to the database!'
error.httpStatusCode = 500
errorMessage = 'Internal DB Error: There are too many connections to the database!'
httpStatusCode = 500
break

default:
error.message = `Unexpected DB Error - Code: ${pgDbError?.code}, Message: ${pgDbError?.message}, Error: ${JSON.stringify(pgDbError)}`
error.httpStatusCode = 503
errorMessage = `Unexpected DB Error - Code: ${pgDbError?.code}, Message: ${pgDbError?.message}, Error: ${JSON.stringify(pgDbError)}`
httpStatusCode = 503
break
}

const error = new Error(errorMessage)
error.httpStatusCode = httpStatusCode
return error
}

Expand Down
32 changes: 32 additions & 0 deletions server/src/api_endpoints/clients/permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const createError = require('http-errors')
const dbAdminPermission = require('database/layer/admin-permission')
const { checkClientExists, checkPermissionNameUniqueness } = require('./utils')

/**
* HTTP Request handler
* Create a permission
*
* @param {Object} req - The request object
* @param {Object} res - The response object to send 201 statusCode and the cerated permission on success
* @param {Function} next - The next middleware function
* @returns {Promise<void>} - A Promise that resolves to void when the permission is created
*/
const createPermission = async (req, res, next) => {
try {
const { client_id: clientId } = req.params
const client = await checkClientExists(clientId)
const payload = {
...req.body,
clientkey: client.key,
}
await checkPermissionNameUniqueness(payload)
const permission = await dbAdminPermission.insertPermission(payload)
res.status(201).send(permission)
} catch ({ httpStatusCode = 500, message }) {
return next(createError(httpStatusCode, { message }))
}
}

module.exports = {
createPermission,
}
35 changes: 30 additions & 5 deletions server/src/api_endpoints/clients/utils.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
const dbAdminRole = require('database/layer/admin-client')
const dbAdminPermission = require('database/layer/admin-permission')

const checkClientExists = async (clientId) => {
try {
await dbAdminRole.getClient(clientId);
return await dbAdminRole.getClient(clientId);
} catch {
throw {
httpStatusCode: 404,
message: 'Client does not exist!',
}
const error = new Error('Client does not exist!')
error.httpStatusCode = 404
throw error
}
}

/**
* Checks the uniqueness of a permission name for a given client key.
*
* This function queries the database to retrieve permissions by name and client key.
* If any permissions are found, it throws an error indicating the name is already taken.
*
* @async
* @function checkPermissionNameUniqueness
* @param {Object} params - The parameters for checking uniqueness.
* @param {string} params.name - The name of the permission to check.
* @param {string} params.clientkey - The client key associated with the permission.
* @throws {Object} Throws an error with HTTP status code 409 if the permission name is not unique.
* @throws {number} error.httpStatusCode - The HTTP status code indicating conflict (409).
* @throws {string} error.message - The error message indicating the permission name is taken.
*/
const checkPermissionNameUniqueness = async ({ name, clientkey: clientKey }) => {
const permissions = await dbAdminPermission.getPermissionsByNameClientKey(name, clientKey);
if (permissions?.length) {
const error = new Error('The permission name is taken!')
error.httpStatusCode = 409
throw error
}
};

module.exports = {
checkClientExists,
checkPermissionNameUniqueness,
}
35 changes: 15 additions & 20 deletions server/src/api_endpoints/personas/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,18 @@ const dbAdminPersonaPermissions = require('database/layer/admin-personapermissio
const checkPersonaExists = async (personaKey) => {
const persona = await dbAdminPersona.getPersona(personaKey)
if (!persona) {
throw {
httpStatusCode: 404,
message: 'Persona does not exist!'
}
const error = new Error('Persona does not exist!')
error.httpStatusCode = 404
throw error
}
}

const checkPermissionExists = async (permissionKey) => {
const permission = await dbAdminPermission.getPermission(permissionKey)
if (!permission) {
throw {
httpStatusCode: 404,
message: 'Permission does not exist!'
}
const error = new Error('Permission does not exist!')
error.httpStatusCode = 404
throw error
}
}

Expand All @@ -35,10 +33,9 @@ const checkPermissionExists = async (permissionKey) => {
const checkPersonaRolesValidity = async (personaKey, roleKeys) => {
const validRoles = await dbAdminPersonaRoles.selectPersonaRolesInTheSameTenant(personaKey, roleKeys)
if (validRoles.length !== roleKeys.length) {
throw {
httpStatusCode: 400,
message: 'Make sure to provide valid role keys which are associated with clients in the same tenant!',
}
const error = new Error('Make sure to provide valid role keys which are associated with clients in the same tenant!')
error.httpStatusCode = 400
throw error
}
}

Expand All @@ -53,20 +50,18 @@ const checkPersonaRolesValidity = async (personaKey, roleKeys) => {
const checkPersonaPermissionsValidity = async (personaKey, permissionKeys) => {
const validPermissions = await dbAdminPersonaPermissions.selectPersonaPermissionsInTheSameTenant(personaKey, permissionKeys)
if (validPermissions.length !== permissionKeys.length) {
throw {
httpStatusCode: 400,
message: 'Make sure to provide valid permission keys which are associated with clients in the same tenant!',
}
const error = new Error('Make sure to provide valid permission keys which are associated with clients in the same tenant!')
error.httpStatusCode = 400
throw error
}
}

const checkRoleExists = async (roleKey) => {
const role = await dbAdminRole.getRole(roleKey)
if (!role) {
throw {
httpStatusCode: 404,
message: 'Role does not exist!'
}
const error = new Error('Role does not exist!')
error.httpStatusCode = 404
throw error
}
}

Expand Down
Loading

0 comments on commit ae1665d

Please sign in to comment.