Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 5 additions & 24 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,12 @@ on:
- main

jobs:
lint:
runs-on: ubuntu-latest

name: Lint

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"

- name: Install dependencies
run: |
npm install

- name: Run lint
run: |
npm run lint

test:
runs-on: ubuntu-latest

strategy:
matrix:
node: [18, 20]
node: [18, 20, 22]

name: Node ${{ matrix.node }}

Expand All @@ -55,6 +32,10 @@ jobs:
run: |
npm install

- name: Lint
run: |
npm run lint

- name: Run tests
run: |
npm run test
11 changes: 6 additions & 5 deletions src/agency.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Brand, Domains, Projects, Roles } from './resources/agency'
import { Brand, Domains, Members, Projects, Roles } from './resources/agency'

import type { Blutui } from './blutui'
import type { GetOptions, PostOptions } from './types'

export class Agency {
readonly brand = new Brand(this)
readonly domains = new Domains(this)
readonly members = new Members(this)
readonly projects = new Projects(this)
readonly roles = new Roles(this)

Expand All @@ -15,15 +16,15 @@ export class Agency {
) {}

async get<Result>(path: string, options: GetOptions = {}) {
return this.blutui.get<Result>(this.getAgencyPath(path), options)
return await this.blutui.get<Result>(this.getAgencyPath(path), options)
}

async post<Result, Entity>(
path: string,
entity: Entity,
options: PostOptions = {}
) {
return this.blutui.post<Result, Entity>(
return await this.blutui.post<Result, Entity>(
this.getAgencyPath(path),
entity,
options
Expand All @@ -35,15 +36,15 @@ export class Agency {
entity: Entity,
options: PostOptions = {}
) {
return this.blutui.patch<Result, Entity>(
return await this.blutui.patch<Result, Entity>(
this.getAgencyPath(path),
entity,
options
)
}

async delete<Result>(path: string, options: PostOptions = {}) {
return this.blutui.delete<Result>(this.getAgencyPath(path), options)
return await this.blutui.delete<Result>(this.getAgencyPath(path), options)
}

/**
Expand Down
8 changes: 6 additions & 2 deletions src/blutui.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Agency } from './agency'
import {
type FetchException,
FetchException,
GenericServerException,
NoAccessTokenProvidedException,
NotFoundException,
Expand Down Expand Up @@ -122,7 +122,7 @@ export class Blutui {
options: PatchOptions = {}
): Promise<{ data: Result }> {
try {
return this.client.patch<Entity>(path, entity, {
return await this.client.patch<Entity>(path, entity, {
params: options.query,
})
} catch (error) {
Expand All @@ -148,6 +148,10 @@ export class Blutui {
}

private handleFetchError({ path, error }: { path: string; error: unknown }) {
if (!(error instanceof FetchException)) {
throw new Error(`Unexpected error: ${error}`)
}

const { response } = error as FetchException<BlutuiResponseError>

if (response) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { deserializePaginationMeta } from '@/utils/serializers'
import { deserializeProject } from '../../projects/serializers'

import type { List, ListResponse } from '@/types'
import type { Domain, DomainResponse } from '../interfaces'
import { deserializeProject } from '../../projects/serializers'

export const deserializeDomain = (domain: DomainResponse): Domain => ({
id: domain.id,
Expand Down
1 change: 1 addition & 0 deletions src/resources/agency/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { Brand } from './brand/brand'
export { Domains } from './domains/domains'
export { Members } from './members/members'
export { Projects } from './projects/projects'
export { Roles } from './roles/roles'
31 changes: 31 additions & 0 deletions src/resources/agency/members/fixtures/member-list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"object": "list",
"data": [
{
"id": "99bc147e-3ad5-4b09-a2e2-b21bd680ad05",
"object": "member",
"name": "Mara Schuppe",
"avatar_url": null,
"email": "mara@blutui.dev",
"two_factor_enabled": false,
"has_full_access": true,
"role": {
"id": 1,
"name": "Owner",
"description": null,
"is_super": true
},
"created_at": 1690330767,
"updated_at": 1720481455
}
],
"meta": {
"hasMore": false,
"currentPage": 1,
"from": 1,
"to": 1,
"perPage": 10,
"total": 1,
"lastPage": 1
}
}
20 changes: 20 additions & 0 deletions src/resources/agency/members/fixtures/member.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"id": "99bc147e-3ad5-4b09-a2e2-b21bd680ad05",
"object": "member",
"name": "Mara Schuppe",
"avatar_url": null,
"email": "mara@blutui.dev",
"two_factor_enabled": false,
"has_full_access": true,
"role": {
"id": 1,
"object": "role",
"name": "Owner",
"description": null,
"is_super": true,
"created_at": 1690330767,
"updated_at": 1690330767
},
"created_at": 1690330767,
"updated_at": 1720481455
}
2 changes: 2 additions & 0 deletions src/resources/agency/members/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './member.interface'
export * from './update-member-options.interface'
27 changes: 27 additions & 0 deletions src/resources/agency/members/interfaces/member.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Role, RoleResponse } from '../../roles/interfaces'

export interface Member {
id: string
object: 'member'
name: string
avatar: string | null
email: string
twoFactorEnabled: boolean
hasFullAccess: boolean
role: Omit<Role, 'permissions'>
createdAt: number
updatedAt: number
}

export interface MemberResponse {
id: string
object: 'member'
name: string
avatar: string | null
email: string
two_factor_enabled: boolean
has_full_access: boolean
role: Omit<RoleResponse, 'permissions'>
created_at: number
updated_at: number
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface UpdateMemberOptions {
role?: number
hasFullAccess?: boolean
}

export interface SerializedUpdateMemberOptions {
role?: number
has_full_access?: boolean
}
85 changes: 85 additions & 0 deletions src/resources/agency/members/members.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import fetch from 'jest-fetch-mock'
import { Blutui } from '@/blutui'
import { fetchOnce, fetchURL } from '@/utils/testing'

import memberFixture from './fixtures/member.json'
import memberListFixture from './fixtures/member-list.json'
import { ValidationException } from '@/exceptions'

const accessToken =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
const blutui = new Blutui(accessToken)

describe('Member', () => {
beforeEach(() => fetch.resetMocks())

describe('list', () => {
it('can retrieve a list of members', async () => {
fetchOnce(memberListFixture)
const members = await blutui.agency('foo').members.list()

expect(fetchURL()).toBe(`${blutui.baseURL}/v1/agencies/foo/members`)
expect(members).toMatchObject({
object: 'list',
data: [{ hasFullAccess: true }],
})
})
})

describe('get', () => {
it('can retrieve a member', async () => {
fetchOnce(memberFixture)
const member = await blutui.agency('foo').members.get(memberFixture.id)

expect(fetchURL()).toBe(
`${blutui.baseURL}/v1/agencies/foo/members/${memberFixture.id}`
)
expect(member).toMatchObject({
object: 'member',
})
})
})

describe('update', () => {
it('can update a member', async () => {
fetchOnce(memberFixture)
const member = await blutui
.agency('foo')
.members.update(memberFixture.id, { role: 2 })

expect(fetchURL()).toBe(
`${blutui.baseURL}/v1/agencies/foo/members/${memberFixture.id}`
)
expect(member).toMatchObject({
object: 'member',
})
})

it('can not update the current users member information', async () => {
fetchOnce(
{ message: 'You can not update your own agency access.' },
{ status: 422 }
)

await expect(
blutui.agency('foo').members.update('my-id', { role: 2 })
).rejects.toThrow(ValidationException)
})
})

describe('remove', () => {
it('can remove a member from an agency', async () => {
fetchOnce({ id: memberFixture.id, object: 'member', deleted: true })
const removedMember = await blutui
.agency('foo')
.members.remove(memberFixture.id)

expect(fetchURL()).toBe(
`${blutui.baseURL}/v1/agencies/foo/members/${memberFixture.id}`
)
expect(removedMember).toMatchObject({
object: 'member',
})
})
})
})
65 changes: 65 additions & 0 deletions src/resources/agency/members/members.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
deserializeMember,
deserializeMemberList,
serializeUpdateMemberOptions,
} from './serializers'

import type { Agency } from '@/agency'
import type {
Member,
MemberResponse,
SerializedUpdateMemberOptions,
UpdateMemberOptions,
} from './interfaces'
import type {
DeletedResponse,
List,
ListResponse,
PaginationOptions,
} from '@/types'

export class Members {
constructor(private readonly agency: Agency) {}

/**
* Get a list of members for the current agency.
*/
async list(options?: PaginationOptions): Promise<List<Member>> {
const { data } = await this.agency.get<ListResponse<MemberResponse>>(
'members',
{ query: options }
)

return deserializeMemberList(data)
}

/**
* Get a member from the current agency.
*/
async get(id: string): Promise<Member> {
const { data } = await this.agency.get<MemberResponse>(`members/${id}`)

return deserializeMember(data)
}

/**
* Update a member of the current agency.
*/
async update(id: string, payload: UpdateMemberOptions): Promise<Member> {
const { data } = await this.agency.patch<
MemberResponse,
SerializedUpdateMemberOptions
>(`members/${id}`, serializeUpdateMemberOptions(payload))

return deserializeMember(data)
}

/**
* Remove a member from the current agency.
*/
async remove(id: string): Promise<DeletedResponse> {
const { data } = await this.agency.delete<DeletedResponse>(`members/${id}`)

return data
}
}
2 changes: 2 additions & 0 deletions src/resources/agency/members/serializers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './member.serializer'
export * from './update-member-options.serializer'
Loading