diff --git a/src/admin.ts b/src/admin.ts new file mode 100644 index 0000000..2e43036 --- /dev/null +++ b/src/admin.ts @@ -0,0 +1,21 @@ +import { Menus } from './resources/project/admin' + +import type { Blutui } from './blutui' +import type { Project } from './project' +import { Request } from './utils/request' + +export class Admin extends Request { + readonly menus = new Menus(this) + + constructor( + private readonly project: Project, + blutui: Blutui + ) { + super(blutui) + } + + protected getRequestPath(path: string): string { + const newPath = path.startsWith('/') ? path.replace('/', '') : path + return `https://${this.project.handle}.blutui.com/admin/api/${newPath}` + } +} diff --git a/src/agency.ts b/src/agency.ts index c057768..2c8a967 100644 --- a/src/agency.ts +++ b/src/agency.ts @@ -9,9 +9,10 @@ import { } from './resources/agency' import type { Blutui } from './blutui' -import type { GetOptions, PostOptions } from './types' -export class Agency { +import { Request } from './utils/request' + +export class Agency extends Request { readonly brand = new Brand(this) readonly cassettes = new Cassettes(this) readonly domains = new Domains(this) @@ -22,45 +23,12 @@ export class Agency { constructor( public username: string, - private readonly blutui: Blutui - ) {} - - async get(path: string, options: GetOptions = {}) { - return await this.blutui.get(this.getAgencyPath(path), options) - } - - async post( - path: string, - entity: Entity, - options: PostOptions = {} + blutui: Blutui ) { - return await this.blutui.post( - this.getAgencyPath(path), - entity, - options - ) - } - - async patch( - path: string, - entity: Entity, - options: PostOptions = {} - ) { - return await this.blutui.patch( - this.getAgencyPath(path), - entity, - options - ) - } - - async delete(path: string, options: PostOptions = {}) { - return await this.blutui.delete(this.getAgencyPath(path), options) + super(blutui) } - /** - * Get the path for the current agency. - */ - private getAgencyPath(path: string): string { + protected getRequestPath(path: string): string { const newPath = path.startsWith('/') ? path.replace('/', '') : path return `/agencies/${this.username}/${newPath}` diff --git a/src/blutui.ts b/src/blutui.ts index 21828cb..d91628a 100644 --- a/src/blutui.ts +++ b/src/blutui.ts @@ -1,4 +1,5 @@ import { Agency } from './agency' +import { Project } from './project' import { FetchException, GenericServerException, @@ -29,6 +30,7 @@ export class Blutui { readonly baseURL: string private readonly client: Client private readonly _agencies: Record = {} + private readonly _projects: Record = {} readonly agencies = new Agencies(this) readonly user = new User(this) @@ -85,6 +87,19 @@ export class Blutui { return this._agencies[username] } + /** + * Get a Blutui Project instance for the given handle. + * + * @param handle - The project's handle. If the project's handle is different from its subdomain, the subdomain should be used instead. + */ + project(handle: string): Project { + if (!this._projects[handle]) { + this._projects[handle] = new Project(handle, this) + } + + return this._projects[handle] + } + async get( path: string, options: GetOptions = {} diff --git a/src/project.ts b/src/project.ts new file mode 100644 index 0000000..c96fa2c --- /dev/null +++ b/src/project.ts @@ -0,0 +1,24 @@ +import { Menus } from './resources/project' + +import type { Blutui } from './blutui' +import { Admin } from './admin' +import { Request } from './utils/request' + +export class Project extends Request { + readonly admin: Admin + readonly menus = new Menus(this) + + constructor( + public handle: string, + blutui: Blutui + ) { + super(blutui) + this.admin = new Admin(this, blutui) + } + + protected getRequestPath(path: string): string { + const newPath = path.startsWith('/') ? path.replace('/', '') : path + + return `https://${this.handle}.blutui.com/api/${newPath}` + } +} diff --git a/src/resources/project/admin.ts b/src/resources/project/admin.ts new file mode 100644 index 0000000..9163b49 --- /dev/null +++ b/src/resources/project/admin.ts @@ -0,0 +1 @@ +export { Menus } from './menus/menus.admin' diff --git a/src/resources/project/index.ts b/src/resources/project/index.ts new file mode 100644 index 0000000..7ba7d9b --- /dev/null +++ b/src/resources/project/index.ts @@ -0,0 +1 @@ +export { Menus } from './menus/menus' diff --git a/src/resources/project/menus/fixtures/menu-list.json b/src/resources/project/menus/fixtures/menu-list.json new file mode 100644 index 0000000..f4feeaa --- /dev/null +++ b/src/resources/project/menus/fixtures/menu-list.json @@ -0,0 +1,21 @@ +{ + "object": "list", + "data": [ + { + "id": "9bfdb42b-1bf0-4510-978e-46aa329f8efa", + "object": "menu", + "name": "Primary Menu", + "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/project/menus/fixtures/menu-with-items.json b/src/resources/project/menus/fixtures/menu-with-items.json new file mode 100644 index 0000000..c9ab0bd --- /dev/null +++ b/src/resources/project/menus/fixtures/menu-with-items.json @@ -0,0 +1,18 @@ +{ + "id": "9bfdb42b-1bf0-4510-978e-46aa329f8efa", + "object": "menu", + "name": "Primary Menu", + "items": [{ + "id": "99bc147e-966c-4dd0-8def-de817c63cf41", + "object": "menu_item", + "label": "Contact", + "url": "/contact", + "active": true, + "is_new_tab": false, + "order": 1, + "created_at": 1720758022, + "updated_at": 1720758046 + }], + "created_at": 1716170007, + "updated_at": 1716170007 +} diff --git a/src/resources/project/menus/fixtures/menu.json b/src/resources/project/menus/fixtures/menu.json new file mode 100644 index 0000000..713e678 --- /dev/null +++ b/src/resources/project/menus/fixtures/menu.json @@ -0,0 +1,7 @@ +{ + "id": "9bfdb42b-1bf0-4510-978e-46aa329f8efa", + "object": "menu", + "name": "Primary Menu", + "created_at": 1716170007, + "updated_at": 1716170007 +} diff --git a/src/resources/project/menus/interfaces/create-menu-options.interface.ts b/src/resources/project/menus/interfaces/create-menu-options.interface.ts new file mode 100644 index 0000000..bce6b59 --- /dev/null +++ b/src/resources/project/menus/interfaces/create-menu-options.interface.ts @@ -0,0 +1,27 @@ +export interface CreateMenuOptions { + handle: string + name: string + items?: CreateMenuItemOptions[] +} + +export interface CreateMenuItemOptions { + label: string + url: string + isNewTab: boolean + active: boolean + items?: CreateMenuItemOptions[] +} + +export interface SerializedCreateMenuOptions { + handle: string + name: string + items?: SerializedCreateMenuItemOptions[] +} + +export interface SerializedCreateMenuItemOptions { + label: string + url: string + is_new_tab: boolean + active: boolean + items?: SerializedCreateMenuItemOptions[] +} diff --git a/src/resources/project/menus/interfaces/index.ts b/src/resources/project/menus/interfaces/index.ts new file mode 100644 index 0000000..76863da --- /dev/null +++ b/src/resources/project/menus/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from './menu.interface' +export * from './create-menu-options.interface' +export * from './update-menu-options.interface' diff --git a/src/resources/project/menus/interfaces/menu.interface.ts b/src/resources/project/menus/interfaces/menu.interface.ts new file mode 100644 index 0000000..4c84c9b --- /dev/null +++ b/src/resources/project/menus/interfaces/menu.interface.ts @@ -0,0 +1,43 @@ +export interface Menu { + id: string + object: 'menu' + name: string + items?: MenuItem[] + createdAt: number + updatedAt: number +} + +export interface MenuResponse { + id: string + object: 'menu' + name: string + items?: MenuItemResponse[] + created_at: number + updated_at: number +} + +export interface MenuItem { + id: string + object: 'menu_item' + label: string + url: string + active: boolean + isNewTab: boolean + order: number + items?: MenuItem[] + createdAt: number + updatedAt: number +} + +export interface MenuItemResponse { + id: string + object: 'menu_item' + label: string + url: string + active: boolean + is_new_tab: boolean + order: number + items?: MenuItemResponse[] + created_at: number + updated_at: number +} diff --git a/src/resources/project/menus/interfaces/update-menu-options.interface.ts b/src/resources/project/menus/interfaces/update-menu-options.interface.ts new file mode 100644 index 0000000..94d4f0b --- /dev/null +++ b/src/resources/project/menus/interfaces/update-menu-options.interface.ts @@ -0,0 +1,27 @@ +export interface UpdateMenuOptions { + handle?: string + name?: string + items?: UpdateMenuItemOptions[] +} + +export interface UpdateMenuItemOptions { + label: string + url: string + isNewTab: boolean + active: boolean + items?: UpdateMenuItemOptions[] +} + +export interface SerializedUpdateMenuOptions { + handle?: string + name?: string + items?: SerializedUpdateMenuItemOptions[] +} + +export interface SerializedUpdateMenuItemOptions { + label: string + url: string + is_new_tab: boolean + active: boolean + items?: SerializedUpdateMenuItemOptions[] +} diff --git a/src/resources/project/menus/menus.admin.spec.ts b/src/resources/project/menus/menus.admin.spec.ts new file mode 100644 index 0000000..3cfda01 --- /dev/null +++ b/src/resources/project/menus/menus.admin.spec.ts @@ -0,0 +1,168 @@ +import fetch from 'jest-fetch-mock' +import { Blutui } from '@/blutui' +import { fetchOnce, fetchURL } from '@/utils/testing' + +import menuListFixture from './fixtures/menu-list.json' +import menuFixture from './fixtures/menu.json' +import menuWithItemsFixture from './fixtures/menu-with-items.json' + +const accessToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' +const blutui = new Blutui(accessToken) + +describe('Menu', () => { + beforeEach(() => fetch.resetMocks()) + + describe('list', () => { + it('can retrieve a list of menus', async () => { + fetchOnce(menuListFixture) + const menus = await blutui.project('foo').admin.menus.list() + + expect(fetchURL()).toBe('https://foo.blutui.com/admin/api/menus') + expect(menus).toMatchObject({ + object: 'list', + }) + }) + }) + + describe('get', () => { + it('can retrieve a menu information', async () => { + fetchOnce(menuFixture) + const menu = await blutui.project('foo').admin.menus.get(menuFixture.id) + + expect(fetchURL()).toBe( + `https://foo.blutui.com/admin/api/menus/${menuFixture.id}` + ) + + expect(menu).toMatchObject({ + object: 'menu', + }) + expect(menu.items).toBe(undefined) + }) + + it('can retrieve a menu information with project', async () => { + fetchOnce(menuWithItemsFixture) + const menu = await blutui + .project('foo') + .admin.menus.get(menuWithItemsFixture.id, { + expand: ['items'], + }) + expect(fetchURL()).toBe( + encodeURI( + `https://foo.blutui.com/admin/api/menus/${menuWithItemsFixture.id}?expand[]=items` + ) + ) + expect(menu).toMatchObject({ + object: 'menu', + }) + + expect(menu.items).toMatchObject([ + { + object: 'menu_item', + id: '99bc147e-966c-4dd0-8def-de817c63cf41', + createdAt: 1720758022, + }, + ]) + }) + }) + + describe('create', () => { + it('can create a new menu', async () => { + fetchOnce(menuFixture) + const menu = await blutui.project('foo').admin.menus.create({ + name: 'Primary Menu', + handle: 'primary-menu', + }) + + expect(fetchURL()).toBe('https://foo.blutui.com/admin/api/menus') + expect(menu).toMatchObject({ + object: 'menu', + }) + }) + + it('can create a new menu with items', async () => { + fetchOnce(menuWithItemsFixture) + const menu = await blutui.project('foo').admin.menus.create({ + name: 'Primary Menu', + handle: 'primary-menu', + items: [ + { + label: 'Contact', + url: '/contact', + isNewTab: false, + active: true, + }, + ], + }) + + expect(fetchURL()).toBe('https://foo.blutui.com/admin/api/menus') + expect(menu).toMatchObject({ + object: 'menu', + items: [ + { + object: 'menu_item', + label: 'Contact', + url: '/contact', + isNewTab: false, + active: true, + }, + ], + }) + }) + }) + + describe('update', () => { + it('can update a menu', async () => { + fetchOnce(menuFixture) + const menu = await blutui + .project('foo') + .admin.menus.update(menuFixture.id, { + name: 'Primary Menu Updated', + handle: 'primary-menu-updated', + }) + + expect(fetchURL()).toBe( + `https://foo.blutui.com/admin/api/menus/${menuFixture.id}` + ) + expect(menu).toMatchObject({ + object: 'menu', + }) + }) + + it('can update a menu with items', async () => { + fetchOnce(menuFixture) + const menu = await blutui + .project('foo') + .admin.menus.update(menuFixture.id, { + name: 'Primary Menu Updated', + handle: 'primary-menu-updated', + items: [ + { + label: 'Contact', + url: '/contact', + isNewTab: false, + active: true, + }, + ], + }) + + expect(fetchURL()).toBe( + `https://foo.blutui.com/admin/api/menus/${menuFixture.id}` + ) + expect(menu).toMatchObject({ + object: 'menu', + }) + }) + }) + + describe('remove', () => { + it('can remove a menu', async () => { + fetchOnce(menuFixture) + await blutui.project('foo').admin.menus.remove(menuFixture.id) + + expect(fetchURL()).toBe( + `https://foo.blutui.com/admin/api/menus/${menuFixture.id}` + ) + }) + }) +}) diff --git a/src/resources/project/menus/menus.admin.ts b/src/resources/project/menus/menus.admin.ts new file mode 100644 index 0000000..016d6a3 --- /dev/null +++ b/src/resources/project/menus/menus.admin.ts @@ -0,0 +1,87 @@ +import type { Project } from '@/project' +import type { + DeletedResponse, + Expandable, + List, + ListResponse, + PaginationOptions, +} from '@/types' +import type { + Menu, + MenuResponse, + CreateMenuOptions, + SerializedCreateMenuOptions, + UpdateMenuOptions, + SerializedUpdateMenuOptions, +} from './interfaces' +import { + deserializeMenu, + deserializeMenuList, + serializeCreateMenuOptions, + serializeUpdateMenuOptions, +} from './serializers' +import type { Admin } from '@/admin' + +export class Menus { + constructor(private readonly project: Project | Admin) {} + + /** + * Get the menu list for the current project. + */ + async list(options?: PaginationOptions): Promise> { + const { data } = await this.project.get>( + 'menus', + { query: options } + ) + + return deserializeMenuList(data) + } + + /** + * Get a menu's information by ID. + */ + async get(id: string, options?: Expandable<'items'>): Promise { + const { data } = await this.project.get(`menus/${id}`, { + query: options, + }) + + return deserializeMenu(data) + } + + /** + * Create a new menu for a project. + * + * @param payload - The values to create the menu + */ + async create(payload: CreateMenuOptions): Promise { + const { data } = await this.project.post< + MenuResponse, + SerializedCreateMenuOptions + >('menus', serializeCreateMenuOptions(payload)) + + return deserializeMenu(data) + } + + /** + * Update the menu for the a project. + * + * @param payload - The values to update the menu + */ + async update(id: string, payload: UpdateMenuOptions): Promise { + const { data } = await this.project.patch< + MenuResponse, + SerializedUpdateMenuOptions + >(`menus/${id}`, serializeUpdateMenuOptions(payload)) + + return deserializeMenu(data) + } + + /** + * Remove the menu for the current project. + */ + async remove(id: string): Promise { + const { data } = await this.project.delete(`menus/${id}`) + + return data + } +} diff --git a/src/resources/project/menus/menus.spec.ts b/src/resources/project/menus/menus.spec.ts new file mode 100644 index 0000000..db35282 --- /dev/null +++ b/src/resources/project/menus/menus.spec.ts @@ -0,0 +1,68 @@ +import fetch from 'jest-fetch-mock' +import { Blutui } from '@/blutui' +import { fetchOnce, fetchURL } from '@/utils/testing' + +import menuListFixture from './fixtures/menu-list.json' +import menuFixture from './fixtures/menu.json' +import menuWithItemsFixture from './fixtures/menu-with-items.json' + +const accessToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' +const blutui = new Blutui(accessToken) + +describe('Menu', () => { + beforeEach(() => fetch.resetMocks()) + + describe('list', () => { + it('can retrieve a list of menus', async () => { + fetchOnce(menuListFixture) + const menus = await blutui.project('foo').menus.list() + + expect(fetchURL()).toBe('https://foo.blutui.com/api/menus') + expect(menus).toMatchObject({ + object: 'list', + }) + }) + }) + + describe('get', () => { + it('can retrieve a menu information', async () => { + fetchOnce(menuFixture) + const menu = await blutui.project('foo').menus.get(menuFixture.id) + + expect(fetchURL()).toBe( + `https://foo.blutui.com/api/menus/${menuFixture.id}` + ) + + expect(menu).toMatchObject({ + object: 'menu', + }) + expect(menu.items).toBe(undefined) + }) + + it('can retrieve a menu information with project', async () => { + fetchOnce(menuWithItemsFixture) + const menu = await blutui + .project('foo') + .menus.get(menuWithItemsFixture.id, { + expand: ['items'], + }) + expect(fetchURL()).toBe( + encodeURI( + `https://foo.blutui.com/api/menus/${menuWithItemsFixture.id}?expand[]=items` + ) + ) + expect(menu).toMatchObject({ + object: 'menu', + }) + + expect(menu.items).toMatchObject([ + { + object: 'menu_item', + id: '99bc147e-966c-4dd0-8def-de817c63cf41', + createdAt: 1720758022, + }, + ]) + }) + }) +}) diff --git a/src/resources/project/menus/menus.ts b/src/resources/project/menus/menus.ts new file mode 100644 index 0000000..df34995 --- /dev/null +++ b/src/resources/project/menus/menus.ts @@ -0,0 +1,32 @@ +import type { Project } from '@/project' +import type { Expandable, List, ListResponse, PaginationOptions } from '@/types' +import type { Menu, MenuResponse } from './interfaces' +import { deserializeMenu, deserializeMenuList } from './serializers' +import type { Admin } from '@/admin' + +export class Menus { + constructor(private readonly project: Project | Admin) {} + + /** + * Get the menu list for the current project. + */ + async list(options?: PaginationOptions): Promise> { + const { data } = await this.project.get>( + 'menus', + { query: options } + ) + + return deserializeMenuList(data) + } + + /** + * Get a menu's information by ID. + */ + async get(id: string, options?: Expandable<'items'>): Promise { + const { data } = await this.project.get(`menus/${id}`, { + query: options, + }) + + return deserializeMenu(data) + } +} diff --git a/src/resources/project/menus/serializers/create-menu-options.serializer.ts b/src/resources/project/menus/serializers/create-menu-options.serializer.ts new file mode 100644 index 0000000..87ccdf4 --- /dev/null +++ b/src/resources/project/menus/serializers/create-menu-options.serializer.ts @@ -0,0 +1,30 @@ +import type { + CreateMenuItemOptions, + CreateMenuOptions, + SerializedCreateMenuItemOptions, + SerializedCreateMenuOptions, +} from '../interfaces' + +export const serializeCreateMenuOptions = ( + options: CreateMenuOptions +): SerializedCreateMenuOptions => ({ + name: options.name, + handle: options.handle, + ...(options.items !== undefined && { + items: serializeCreateMenuItemOptions(options.items), + }), +}) + +export const serializeCreateMenuItemOptions = ( + items: CreateMenuItemOptions[] +): SerializedCreateMenuItemOptions[] => { + return items.map((item) => ({ + label: item.label, + url: item.url, + is_new_tab: item.isNewTab, + active: item.active, + ...(item.items !== undefined && { + items: serializeCreateMenuItemOptions(item.items), + }), + })) +} diff --git a/src/resources/project/menus/serializers/index.ts b/src/resources/project/menus/serializers/index.ts new file mode 100644 index 0000000..8be2d31 --- /dev/null +++ b/src/resources/project/menus/serializers/index.ts @@ -0,0 +1,3 @@ +export * from './menu.serializer' +export * from './create-menu-options.serializer' +export * from './update-menu-options.serializer' diff --git a/src/resources/project/menus/serializers/menu.serializer.ts b/src/resources/project/menus/serializers/menu.serializer.ts new file mode 100644 index 0000000..4c311e0 --- /dev/null +++ b/src/resources/project/menus/serializers/menu.serializer.ts @@ -0,0 +1,41 @@ +import { deserializePaginationMeta } from '@/utils/serializers' + +import type { List, ListResponse } from '@/types' +import type { + Menu, + MenuItem, + MenuItemResponse, + MenuResponse, +} from '../interfaces' + +export const deserializeMenu = (menu: MenuResponse): Menu => ({ + id: menu.id, + object: menu.object, + name: menu.name, + ...(menu.items !== undefined && { items: deserializeMenuItem(menu.items) }), + createdAt: menu.created_at, + updatedAt: menu.updated_at, +}) + +export const deserializeMenuItem = (items: MenuItemResponse[]): MenuItem[] => { + return items.map((item) => ({ + id: item.id, + object: item.object, + label: item.label, + url: item.url, + active: item.active, + isNewTab: item.is_new_tab, + order: item.order, + ...(item.items !== undefined && { items: deserializeMenuItem(item.items) }), + createdAt: item.created_at, + updatedAt: item.updated_at, + })) +} + +export const deserializeMenuList = ( + menus: ListResponse +): List => ({ + object: menus.object, + data: menus.data.map(deserializeMenu), + meta: deserializePaginationMeta(menus.meta), +}) diff --git a/src/resources/project/menus/serializers/update-menu-options.serializer.ts b/src/resources/project/menus/serializers/update-menu-options.serializer.ts new file mode 100644 index 0000000..fa6fdd4 --- /dev/null +++ b/src/resources/project/menus/serializers/update-menu-options.serializer.ts @@ -0,0 +1,30 @@ +import type { + SerializedUpdateMenuItemOptions, + SerializedUpdateMenuOptions, + UpdateMenuItemOptions, + UpdateMenuOptions, +} from '../interfaces' + +export const serializeUpdateMenuOptions = ( + options: UpdateMenuOptions +): SerializedUpdateMenuOptions => ({ + name: options.name, + handle: options.handle, + ...(options.items !== undefined && { + items: serializeUpdateMenuItemOptions(options.items), + }), +}) + +export const serializeUpdateMenuItemOptions = ( + items: UpdateMenuItemOptions[] +): SerializedUpdateMenuItemOptions[] => { + return items.map((item) => ({ + label: item.label, + url: item.url, + is_new_tab: item.isNewTab, + active: item.active, + ...(item.items !== undefined && { + items: serializeUpdateMenuItemOptions(item.items), + }), + })) +} diff --git a/src/resources/site/index.ts b/src/resources/site/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/client.ts b/src/utils/client.ts index 0c77d50..69a501d 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -90,7 +90,12 @@ export class Client { private getResourceURL(path: string, params?: Record) { const queryString = getQueryString(params) const url = new URL( - [this.pathUsingVersion(path), queryString].filter(Boolean).join('?'), + [ + path.startsWith('http') ? path : this.pathUsingVersion(path), + queryString, + ] + .filter(Boolean) + .join('?'), this.baseURL ) return url.toString() diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..ba5c0b4 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,43 @@ +import type { Blutui } from '../blutui' +import type { GetOptions, PostOptions } from '../types' + +export abstract class Request { + constructor(private readonly blutui: Blutui) {} + + async get(path: string, options: GetOptions = {}) { + return await this.blutui.get(this.getRequestPath(path), options) + } + + async post( + path: string, + entity: Entity, + options: PostOptions = {} + ) { + return await this.blutui.post( + this.getRequestPath(path), + entity, + options + ) + } + + async patch( + path: string, + entity: Entity, + options: PostOptions = {} + ) { + return await this.blutui.patch( + this.getRequestPath(path), + entity, + options + ) + } + + async delete(path: string, options: PostOptions = {}) { + return await this.blutui.delete(this.getRequestPath(path), options) + } + + /** + * Get the path for the current request. + */ + protected abstract getRequestPath(path: string): string +}