diff --git a/src/agency.ts b/src/agency.ts index 16d17c4..57a7440 100644 --- a/src/agency.ts +++ b/src/agency.ts @@ -1,4 +1,4 @@ -import { Brand, Domains, Projects } from './resources/agency' +import { Brand, Domains, Projects, Roles } from './resources/agency' import type { Blutui } from './blutui' import type { GetOptions, PostOptions } from './types' @@ -7,6 +7,7 @@ export class Agency { readonly brand = new Brand(this) readonly domains = new Domains(this) readonly projects = new Projects(this) + readonly roles = new Roles(this) constructor( public username: string, diff --git a/src/resources/agency/domains/domains.ts b/src/resources/agency/domains/domains.ts index 6363625..3dfcb19 100644 --- a/src/resources/agency/domains/domains.ts +++ b/src/resources/agency/domains/domains.ts @@ -66,7 +66,7 @@ export class Domains { /** * Update a domain for the current agency. * - * @param payload - The values to update the brand + * @param payload - The values to update the domain */ async update(id: string, payload: UpdateDomainOptions): Promise { const { data } = await this.agency.patch< diff --git a/src/resources/agency/index.ts b/src/resources/agency/index.ts index 8b83505..9b212cb 100644 --- a/src/resources/agency/index.ts +++ b/src/resources/agency/index.ts @@ -1,3 +1,4 @@ export { Brand } from './brand/brand' export { Domains } from './domains/domains' export { Projects } from './projects/projects' +export { Roles } from './roles/roles' diff --git a/src/resources/agency/roles/fixtures/role-list.json b/src/resources/agency/roles/fixtures/role-list.json new file mode 100644 index 0000000..03e3ed0 --- /dev/null +++ b/src/resources/agency/roles/fixtures/role-list.json @@ -0,0 +1,22 @@ +{ + "object": "list", + "data": [ + { + "id": "9bfdb42b-1bf0-4510-978e-46aa329f8efa", + "object": "role", + "name": "Owner", + "description": "Owner of the domain", + "created_at": 1716170007, + "updated_at": 1716170007 + } + ], + "meta": { + "hasMore": false, + "currentPage": 1, + "from": 1, + "to": 1, + "perPage": 10, + "total": 1, + "lastPage": 1 + } +} diff --git a/src/resources/agency/roles/fixtures/role.json b/src/resources/agency/roles/fixtures/role.json new file mode 100644 index 0000000..af87179 --- /dev/null +++ b/src/resources/agency/roles/fixtures/role.json @@ -0,0 +1,12 @@ +{ + "id": "9bfdb42b-1bf0-4510-978e-46aa329f8efa", + "object": "role", + "name": "Owner", + "description": "Owner of the domain", + "permissions": { + "user.read": true, + "user.write": false + }, + "created_at": 1716170007, + "updated_at": 1716170007 +} diff --git a/src/resources/agency/roles/interfaces/create-role-options.interface.ts b/src/resources/agency/roles/interfaces/create-role-options.interface.ts new file mode 100644 index 0000000..b584d1d --- /dev/null +++ b/src/resources/agency/roles/interfaces/create-role-options.interface.ts @@ -0,0 +1,17 @@ +import type { Permission } from './role.interface' + +export interface CreateRoleOptions { + name: string + description?: string | null + permissions?: { + [key in Permission]: boolean + } +} + +export interface SerializedCreateRoleOptions { + name: string + description?: string | null + permissions?: { + [key in Permission]: boolean + } +} diff --git a/src/resources/agency/roles/interfaces/index.ts b/src/resources/agency/roles/interfaces/index.ts new file mode 100644 index 0000000..014de32 --- /dev/null +++ b/src/resources/agency/roles/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from './role.interface' +export * from './create-role-options.interface' +export * from './update-role-options.interface' diff --git a/src/resources/agency/roles/interfaces/role.interface.ts b/src/resources/agency/roles/interfaces/role.interface.ts new file mode 100644 index 0000000..3adb533 --- /dev/null +++ b/src/resources/agency/roles/interfaces/role.interface.ts @@ -0,0 +1,42 @@ +export type Permission = + | 'domain.delete' + | 'domain.read' + | 'domain.write' + | 'organization.delete' + | 'organization.read' + | 'organization.write' + | 'site.delete' + | 'site.export' + | 'site.publish' + | 'site.read' + | 'site.transfer' + | 'site.write' + | 'stripe' + | 'user.admin' + | 'user.delete' + | 'user.invite' + | 'user.read' + +export interface Role { + id: string + object: 'role' + name: string + description: string + isSuper: boolean + usersCount?: number + permissions: { [key in Permission]: boolean } + createdAt: number + updatedAt: number +} + +export interface RoleResponse { + id: string + object: 'role' + name: string + description: string + is_super: boolean + users_count?: number + permissions: { [key in Permission]: boolean } + created_at: number + updated_at: number +} diff --git a/src/resources/agency/roles/interfaces/update-role-options.interface.ts b/src/resources/agency/roles/interfaces/update-role-options.interface.ts new file mode 100644 index 0000000..724af01 --- /dev/null +++ b/src/resources/agency/roles/interfaces/update-role-options.interface.ts @@ -0,0 +1,17 @@ +import type { Permission } from './role.interface' + +export interface UpdateRoleOptions { + name?: string + description?: string | null + permissions?: { + [key in Permission]: boolean + } +} + +export interface SerializedUpdateRoleOptions { + name?: string + description?: string | null + permissions?: { + [key in Permission]: boolean + } +} diff --git a/src/resources/agency/roles/roles.spec.ts b/src/resources/agency/roles/roles.spec.ts new file mode 100644 index 0000000..e702340 --- /dev/null +++ b/src/resources/agency/roles/roles.spec.ts @@ -0,0 +1,82 @@ +import fetch from 'jest-fetch-mock' +import { Blutui } from '@/blutui' +import { fetchOnce, fetchURL } from '@/utils/testing' + +import roleFixture from './fixtures/role.json' +import roleListFixture from './fixtures/role-list.json' + +const accessToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' +const blutui = new Blutui(accessToken) + +describe('Role', () => { + beforeEach(() => fetch.resetMocks()) + + describe('list', () => { + it('can retrieve a list of roles', async () => { + fetchOnce(roleListFixture) + const domains = await blutui.agency('foo').roles.list() + + expect(fetchURL()).toBe(`${blutui.baseURL}/v1/agencies/foo/roles`) + expect(domains).toMatchObject({ + object: 'list', + }) + }) + }) + + describe('get', () => { + it('can retrieve a role information', async () => { + fetchOnce(roleFixture) + const role = await blutui.agency('foo').roles.get(roleFixture.id) + + expect(fetchURL()).toBe( + `${blutui.baseURL}/v1/agencies/foo/roles/${roleFixture.id}` + ) + expect(role).toMatchObject({ + object: 'role', + }) + }) + }) + + describe('create', () => { + it('can create a new role', async () => { + fetchOnce(roleFixture) + const role = await blutui.agency('foo').roles.create({ + name: 'Owner', + }) + + expect(fetchURL()).toBe(`${blutui.baseURL}/v1/agencies/foo/roles`) + expect(role).toMatchObject({ + object: 'role', + name: 'Owner', + }) + }) + }) + + describe('update', () => { + it('can update an agency role', async () => { + fetchOnce(roleFixture) + const role = await blutui + .agency('foo') + .roles.update(roleFixture.id, { name: 'New Role Name' }) + + expect(fetchURL()).toBe( + `${blutui.baseURL}/v1/agencies/foo/roles/${roleFixture.id}` + ) + expect(role).toMatchObject({ + object: 'role', + }) + }) + }) + + describe('remove', () => { + it('can remove an role', async () => { + fetchOnce(roleFixture) + await blutui.agency('foo').roles.remove(roleFixture.id) + + expect(fetchURL()).toBe( + `${blutui.baseURL}/v1/agencies/foo/roles/${roleFixture.id}` + ) + }) + }) +}) diff --git a/src/resources/agency/roles/roles.ts b/src/resources/agency/roles/roles.ts new file mode 100644 index 0000000..c499486 --- /dev/null +++ b/src/resources/agency/roles/roles.ts @@ -0,0 +1,82 @@ +import { + deserializeRole, + deserializeRoleList, + serializeCreateRoleOptions, + serializeUpdateRoleOptions, +} from './serializers' + +import type { Agency } from '@/agency' +import type { + CreateRoleOptions, + Role, + RoleResponse, + SerializedCreateRoleOptions, + SerializedUpdateRoleOptions, + UpdateRoleOptions, +} from './interfaces' +import type { + DeletedResponse, + List, + ListResponse, + PaginationOptions, +} from '@/types' + +export class Roles { + constructor(private readonly agency: Agency) {} + + /** + * Get a list of roles for the current agency. + */ + async list(options?: PaginationOptions): Promise> { + const { data } = await this.agency.get>( + 'roles', + { query: options } + ) + + return deserializeRoleList(data) + } + + /** + * Get a role's information by ID. + */ + async get(id: string): Promise { + const { data } = await this.agency.get(`roles/${id}`) + + return deserializeRole(data) + } + + /** + * Add a role to your agency. + */ + async create(payload: CreateRoleOptions): Promise { + const { data } = await this.agency.post< + RoleResponse, + SerializedCreateRoleOptions + >('roles', serializeCreateRoleOptions(payload)) + + return deserializeRole(data) + } + + /** + * Update a role in the current agency. + * + * @param payload - The values to update the role + */ + async update(id: string, payload: UpdateRoleOptions): Promise { + const { data } = await this.agency.patch< + RoleResponse, + SerializedUpdateRoleOptions + >(`roles/${id}`, serializeUpdateRoleOptions(payload)) + + return deserializeRole(data) + } + + /** + * Remove a role from the current agency. + */ + async remove(id: string): Promise { + const { data } = await this.agency.delete(`roles/${id}`) + + return data + } +} diff --git a/src/resources/agency/roles/serializers/create-role-options.serializer.ts b/src/resources/agency/roles/serializers/create-role-options.serializer.ts new file mode 100644 index 0000000..87f6607 --- /dev/null +++ b/src/resources/agency/roles/serializers/create-role-options.serializer.ts @@ -0,0 +1,12 @@ +import type { + CreateRoleOptions, + SerializedCreateRoleOptions, +} from '../interfaces' + +export const serializeCreateRoleOptions = ( + options: CreateRoleOptions +): SerializedCreateRoleOptions => ({ + name: options.name, + description: options.description, + permissions: options.permissions, +}) diff --git a/src/resources/agency/roles/serializers/index.ts b/src/resources/agency/roles/serializers/index.ts new file mode 100644 index 0000000..5273321 --- /dev/null +++ b/src/resources/agency/roles/serializers/index.ts @@ -0,0 +1,3 @@ +export * from './role.serializer' +export * from './create-role-options.serializer' +export * from './update-role-options.serializer' diff --git a/src/resources/agency/roles/serializers/role.serializer.ts b/src/resources/agency/roles/serializers/role.serializer.ts new file mode 100644 index 0000000..14ac59a --- /dev/null +++ b/src/resources/agency/roles/serializers/role.serializer.ts @@ -0,0 +1,38 @@ +import { deserializePaginationMeta } from '@/utils/serializers' + +import type { List, ListResponse } from '@/types' +import type { Role, RoleResponse } from '../interfaces' + +export const deserializeRole = (role: RoleResponse): Role => { + const { + id, + object, + name, + description, + is_super: isSuper, + permissions, + created_at: createdAt, + updated_at: updatedAt, + users_count: usersCount, + } = role + + return { + id, + object, + name, + description, + isSuper, + permissions, + createdAt, + updatedAt, + ...(usersCount !== undefined && { usersCount }), + } +} + +export const deserializeRoleList = ( + roles: ListResponse +): List => ({ + object: 'list', + data: roles.data.map(deserializeRole), + meta: deserializePaginationMeta(roles.meta), +}) diff --git a/src/resources/agency/roles/serializers/update-role-options.serializer.ts b/src/resources/agency/roles/serializers/update-role-options.serializer.ts new file mode 100644 index 0000000..e1b9193 --- /dev/null +++ b/src/resources/agency/roles/serializers/update-role-options.serializer.ts @@ -0,0 +1,12 @@ +import type { + SerializedUpdateRoleOptions, + UpdateRoleOptions, +} from '../interfaces' + +export const serializeUpdateRoleOptions = ( + options: UpdateRoleOptions +): SerializedUpdateRoleOptions => ({ + name: options.name, + description: options.description, + permissions: options.permissions, +})