diff --git a/package.json b/package.json index f380b54..75689c9 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "scripts": { "build": "tsup src/index.ts --format esm,cjs --dts", + "dev": "npm run build -- --watch", "test": "jest", "format": "prettier \"src/**/*.{js,ts}\" --write", "prepublishOnly": "npm run build" diff --git a/src/agency.ts b/src/agency.ts index 0069314..801980e 100644 --- a/src/agency.ts +++ b/src/agency.ts @@ -1,10 +1,11 @@ import { Blutui } from './blutui' -import { Brand } from './resources/agency' +import { Brand, Domains } from './resources/agency' import type { GetOptions, PostOptions } from './types' export class Agency { readonly brand = new Brand(this) + readonly domains = new Domains(this) constructor( public username: string, diff --git a/src/resources/agency/domains/domains.spec.ts b/src/resources/agency/domains/domains.spec.ts new file mode 100644 index 0000000..082fc39 --- /dev/null +++ b/src/resources/agency/domains/domains.spec.ts @@ -0,0 +1,127 @@ +import fetch from 'jest-fetch-mock' +import { Blutui } from '@/blutui' +import { fetchOnce, fetchURL } from '@/utils/testing' +import domainFixture from './fixtures/domain.json' +import domainListFixture from './fixtures/domainList.json' + +const accessToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' +const blutui = new Blutui(accessToken) + +describe('Domain', () => { + beforeEach(() => fetch.resetMocks()) + + describe('list', () => { + it('can retrieve a list of domains', async () => { + fetchOnce(domainListFixture) + const domains = await blutui.agency('foo').domains.list() + + expect(fetchURL()).toBe(blutui.baseURL + `/v1/agencies/foo/domains`) + expect(domains).toMatchObject({ + object: 'list', + }) + }) + }) + + describe('get', () => { + it('can retrieve a domain information', async () => { + fetchOnce(domainFixture) + const domain = await blutui.agency('foo').domains.get(domainFixture.id) + + expect(fetchURL()).toBe( + blutui.baseURL + `/v1/agencies/foo/domains/${domainFixture.id}` + ) + expect(domain).toMatchObject({ + object: 'domain', + }) + }) + + it('can retrieve a domain information with project', async () => { + fetchOnce(domainFixture) + const domain = await blutui.agency('foo').domains.get(domainFixture.id, { + expand: ['project'], + }) + expect(fetchURL()).toContain( + encodeURI( + `/v1/agencies/foo/domains/${domainFixture.id}?expand[]=project` + ) + ) + expect(domain).toMatchObject({ + object: 'domain', + }) + }) + }) + + describe('create', () => { + it('can create a new agency domain', async () => { + fetchOnce(domainFixture) + const domain = await blutui.agency('foo').domains.create({ + name: 'example.com', + }) + + expect(fetchURL()).toBe(blutui.baseURL + '/v1/agencies/foo/domains') + expect(domain).toMatchObject({ + object: 'domain', + name: 'example.com', + }) + }) + }) + + describe('update', () => { + it('can update an agency domain', async () => { + fetchOnce(domainFixture) + const domain = await blutui + .agency('foo') + .domains.update(domainFixture.id, { project: 'project-uuid' }) + + expect(fetchURL()).toBe(blutui.baseURL + `/v1/agencies/foo/domains/${domainFixture.id}`) + expect(domain).toMatchObject({ + object: 'domain', + }) + }) + }) + + describe('remove', () => { + it('can remove an agency domain', async () => { + fetchOnce(domainFixture) + await blutui.agency('foo').domains.remove(domainFixture.id) + + expect(fetchURL()).toBe( + blutui.baseURL + `/v1/agencies/foo/domains/${domainFixture.id}` + ) + }) + }) + + describe('refresh', () => { + it('can refresh domain token', async () => { + fetchOnce(domainFixture) + await blutui.agency('foo').domains.refresh(domainFixture.id) + + expect(fetchURL()).toBe( + blutui.baseURL + `/v1/agencies/foo/domains/${domainFixture.id}/refresh` + ) + }) + }) + + describe('verify', () => { + it('can verify domain status', async () => { + fetchOnce(domainFixture) + await blutui.agency('foo').domains.verify(domainFixture.id) + + expect(fetchURL()).toBe( + blutui.baseURL + `/v1/agencies/foo/domains/${domainFixture.id}/verify` + ) + }) + }) + + describe('search', () => { + it('Search for domains in your agency.', async () => { + fetchOnce(domainListFixture) + await blutui.agency('foo').domains.search({"name": "example.com"}) + + expect(fetchURL()).toBe( + blutui.baseURL + `/v1/agencies/foo/domains/search` + ) + }) + }) +}) diff --git a/src/resources/agency/domains/domains.ts b/src/resources/agency/domains/domains.ts index f84c297..e3c0253 100644 --- a/src/resources/agency/domains/domains.ts +++ b/src/resources/agency/domains/domains.ts @@ -1,5 +1,115 @@ -// import { Agency } from "@/agency"; +import { Agency } from '@/agency' +import { + CreateDomainOptions, + DomainResponse, + DomainVerifyResponse, + SearchDomainOptions, + SerializedCreateDomainOptions, + SerializedUpdateDomainOptions, + UpdateDomainOptions, + type Domain, +} from './interfaces' +import { + deserializeDomain, + deserializeDomainList, + serializeCreateDomainOptions, + serializeUpdateDomainOptions, +} from './serializers' +import { DeletedResponse, List, ListResponse, PaginationOptions } from '@/types' export class Domains { - // constructor(private readonly agency: Agency) {} + constructor(private readonly agency: Agency) {} + + /** + * Get the domains list for the current agency. + */ + async list(paginationOptions?: PaginationOptions): Promise> { + const { data } = await this.agency.get>( + 'domains', + { query: paginationOptions } + ) + return deserializeDomainList(data) + } + + /** + * Get a domain information by id. + */ + async get(id: string, options?: { expand: string[] }): Promise { + const { data } = await this.agency.get(`domains/${id}`, { + query: options, + }) + + return deserializeDomain(data) + } + + /** + * Add a domain to your agency. + */ + async create(payload: CreateDomainOptions): Promise { + const { data } = await this.agency.post< + DomainResponse, + SerializedCreateDomainOptions + >('domains', serializeCreateDomainOptions(payload)) + + return deserializeDomain(data) + } + + /** + * Update a domain for the current agency. + * + * @param payload - The values to update the brand + */ + async update(id: string, payload: UpdateDomainOptions): Promise { + const { data } = await this.agency.patch< + DomainResponse, + SerializedUpdateDomainOptions + >(`domains/${id}`, serializeUpdateDomainOptions(payload)) + + return deserializeDomain(data) + } + + /** + * Remove a domain for the current agency. + */ + async remove(id: string): Promise { + const { data } = await this.agency.delete(`domains/${id}`) + + return data + } + + /** + * Refresh the verification token for a domain from your agency. + */ + async refresh(id: string): Promise { + const { data } = await this.agency.post( + `domains/${id}/refresh`, + {} + ) + + return deserializeDomain(data) + } + + /** + * Check the verification status for a domain in your agency. + */ + async verify(id: string): Promise { + const { data } = await this.agency.post( + `domains/${id}/verify`, + {} + ) + return data + } + + /** + * Search for domains in your agency. + */ + async search( + searchDomainOptions: SearchDomainOptions + ): Promise> { + const { data } = await this.agency.post>( + 'domains/search', + searchDomainOptions + ) + return deserializeDomainList(data) + } } diff --git a/src/resources/agency/domains/fixtures/domain.json b/src/resources/agency/domains/fixtures/domain.json new file mode 100644 index 0000000..06601d6 --- /dev/null +++ b/src/resources/agency/domains/fixtures/domain.json @@ -0,0 +1,10 @@ +{ + "id": "9bfdb42b-1bf0-4510-978e-46aa329f8efa", + "object": "domain", + "name": "example.com", + "token": "08Q8wwsDMIuCwAsudZtXgf3ABkAwqbExgUPWUEPEuMBoWUIH0ie81R27h2elqjk1", + "project": "9bf15409-db06-4c2c-b1ee-3a64c0074092", + "verified_at": null, + "created_at": 1716170007, + "updated_at": 1716170007 +} diff --git a/src/resources/agency/domains/fixtures/domainList.json b/src/resources/agency/domains/fixtures/domainList.json new file mode 100644 index 0000000..6b1d711 --- /dev/null +++ b/src/resources/agency/domains/fixtures/domainList.json @@ -0,0 +1,24 @@ +{ + "object": "list", + "data": [ + { + "id": "9bfdb42b-1bf0-4510-978e-46aa329f8efa", + "object": "domain", + "name": "example.com", + "token": "08Q8wwsDMIuCwAsudZtXgf3ABkAwqbExgUPWUEPEuMBoWUIH0ie81R27h2elqjk1", + "project": "9bf15409-db06-4c2c-b1ee-3a64c0074092", + "verified_at": null, + "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/domains/interfaces/create-domain-options.interface.ts b/src/resources/agency/domains/interfaces/create-domain-options.interface.ts new file mode 100644 index 0000000..6e90bfe --- /dev/null +++ b/src/resources/agency/domains/interfaces/create-domain-options.interface.ts @@ -0,0 +1,9 @@ +export interface CreateDomainOptions { + name: string + project?: string | null +} + +export interface SerializedCreateDomainOptions { + name: string + project?: string | null +} diff --git a/src/resources/agency/domains/interfaces/domain.interface.ts b/src/resources/agency/domains/interfaces/domain.interface.ts new file mode 100644 index 0000000..c41189e --- /dev/null +++ b/src/resources/agency/domains/interfaces/domain.interface.ts @@ -0,0 +1,21 @@ +export interface Domain { + id: string + object: 'domain' + name: string + token: string + project: string | Record + verifiedAt: number + createdAt: number + updatedAt: number +} + +export interface DomainResponse { + id: string + object: 'domain' + name: string + token: string + project: string | Record + verified_at: number + created_at: number + updated_at: number +} diff --git a/src/resources/agency/domains/interfaces/index.ts b/src/resources/agency/domains/interfaces/index.ts new file mode 100644 index 0000000..a47348f --- /dev/null +++ b/src/resources/agency/domains/interfaces/index.ts @@ -0,0 +1,5 @@ +export * from './domain.interface' +export * from './create-domain-options.interface' +export * from './update-domain-options.interface' +export * from './search-domain-options.interface' +export * from './verify-domain-response.interface' diff --git a/src/resources/agency/domains/interfaces/search-domain-options.interface.ts b/src/resources/agency/domains/interfaces/search-domain-options.interface.ts new file mode 100644 index 0000000..f4e5537 --- /dev/null +++ b/src/resources/agency/domains/interfaces/search-domain-options.interface.ts @@ -0,0 +1,7 @@ +export interface SearchDomainOptions { + name: string +} + +export interface SerializedSearchDomainOptions { + name: string +} diff --git a/src/resources/agency/domains/interfaces/update-domain-options.interface.ts b/src/resources/agency/domains/interfaces/update-domain-options.interface.ts new file mode 100644 index 0000000..919eece --- /dev/null +++ b/src/resources/agency/domains/interfaces/update-domain-options.interface.ts @@ -0,0 +1,7 @@ +export interface UpdateDomainOptions { + project: string | null +} + +export interface SerializedUpdateDomainOptions { + project: string | null +} diff --git a/src/resources/agency/domains/interfaces/verify-domain-response.interface.ts b/src/resources/agency/domains/interfaces/verify-domain-response.interface.ts new file mode 100644 index 0000000..212b45b --- /dev/null +++ b/src/resources/agency/domains/interfaces/verify-domain-response.interface.ts @@ -0,0 +1,7 @@ +// Domain Verify Response + +export interface DomainVerifyResponse { + object: 'domain_state' + verified: boolean + message: string +} diff --git a/src/resources/agency/domains/serializers/create-domain-options.serializer.ts b/src/resources/agency/domains/serializers/create-domain-options.serializer.ts new file mode 100644 index 0000000..ecb2a21 --- /dev/null +++ b/src/resources/agency/domains/serializers/create-domain-options.serializer.ts @@ -0,0 +1,11 @@ +import { + CreateDomainOptions, + SerializedCreateDomainOptions, +} from '../interfaces' + +export const serializeCreateDomainOptions = ( + options: CreateDomainOptions +): SerializedCreateDomainOptions => ({ + name: options.name, + project: options.project, +}) diff --git a/src/resources/agency/domains/serializers/domain.serializer.ts b/src/resources/agency/domains/serializers/domain.serializer.ts new file mode 100644 index 0000000..ad669ea --- /dev/null +++ b/src/resources/agency/domains/serializers/domain.serializer.ts @@ -0,0 +1,22 @@ +import { List, ListResponse } from '@/types' +import { Domain, DomainResponse } from '../interfaces' +import { deserializePaginationMeta } from '@/utils/serializers' + +export const deserializeDomain = (domain: DomainResponse): Domain => ({ + id: domain.id, + object: domain.object, + name: domain.name, + token: domain.token, + project: domain.project, + verifiedAt: domain.verified_at, + createdAt: domain.created_at, + updatedAt: domain.updated_at, +}) + +export const deserializeDomainList = ( + domains: ListResponse +): List => ({ + object: 'list', + data: domains.data.map(deserializeDomain), + meta: deserializePaginationMeta(domains.meta), +}) diff --git a/src/resources/agency/domains/serializers/index.ts b/src/resources/agency/domains/serializers/index.ts new file mode 100644 index 0000000..a6ba20e --- /dev/null +++ b/src/resources/agency/domains/serializers/index.ts @@ -0,0 +1,3 @@ +export * from './domain.serializer' +export * from './create-domain-options.serializer' +export * from './update-domain-options.serializer' diff --git a/src/resources/agency/domains/serializers/update-domain-options.serializer.ts b/src/resources/agency/domains/serializers/update-domain-options.serializer.ts new file mode 100644 index 0000000..577e475 --- /dev/null +++ b/src/resources/agency/domains/serializers/update-domain-options.serializer.ts @@ -0,0 +1,10 @@ +import { + SerializedUpdateDomainOptions, + UpdateDomainOptions, +} from '../interfaces' + +export const serializeUpdateDomainOptions = ( + options: UpdateDomainOptions +): SerializedUpdateDomainOptions => ({ + project: options.project, +}) diff --git a/src/resources/agency/project/interfaces/index.ts b/src/resources/agency/project/interfaces/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/resources/agency/project/interfaces/project.interface.ts b/src/resources/agency/project/interfaces/project.interface.ts new file mode 100644 index 0000000..7b43950 --- /dev/null +++ b/src/resources/agency/project/interfaces/project.interface.ts @@ -0,0 +1,13 @@ +export interface Project { + id: string + object: 'project' + name: string + description: string +} + +export interface ProjectResponse { + id: string + object: 'project' + name: string + description: string +} diff --git a/src/types.ts b/src/types.ts index 60689b9..5d81eb0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,46 @@ export interface BlutuiOptions {} +// List + +export interface List { + object: 'list' + data: T[] + meta: PaginationMeta +} + +export interface ListResponse { + object: 'list' + data: T[] + meta: PaginationMetaResponse +} + +// Pagination + +export interface PaginationMeta { + hasMore: boolean + currentPage: number + from: number + to: number + total: number + perPage: number + lastPage: number +} + +export interface PaginationMetaResponse { + has_more: boolean + current_page: number + from: number + to: number + total: number + per_page: number + last_page: number +} + +export interface PaginationOptions { + limit?: number + page?: number +} + // Request Options export interface GetOptions { diff --git a/src/utils/client.ts b/src/utils/client.ts index bcae148..542d330 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -59,8 +59,7 @@ export class Client { } private getResourceURL(path: string, params?: Record) { - const queryString = params - + const queryString = getQueryString(params) const url = new URL( [this.pathUsingVersion(path), queryString].filter(Boolean).join('?'), this.baseURL @@ -107,6 +106,24 @@ export class Client { } } +function getQueryString(queryObj?: Record) { + if (!queryObj) return undefined + + const sanitizedQueryObj: string[][] = [] + + Object.entries(queryObj).forEach(([param, value]) => { + if (Array.isArray(value)) { + value.forEach((element) => { + if (element !== '' && element !== undefined) + sanitizedQueryObj.push([`${param}[]`, element]) + }) + } else if (value !== '' && value !== undefined) + sanitizedQueryObj.push([param, value]) + }) + + return new URLSearchParams(sanitizedQueryObj).toString() +} + function getBody(entity: any): BodyInit | null | undefined { return JSON.stringify(entity) } diff --git a/src/utils/serializers/index.ts b/src/utils/serializers/index.ts new file mode 100644 index 0000000..8a1efcc --- /dev/null +++ b/src/utils/serializers/index.ts @@ -0,0 +1 @@ +export * from './pagination.serializer' diff --git a/src/utils/serializers/pagination.serializer.ts b/src/utils/serializers/pagination.serializer.ts new file mode 100644 index 0000000..2f1853e --- /dev/null +++ b/src/utils/serializers/pagination.serializer.ts @@ -0,0 +1,13 @@ +import { PaginationMeta, PaginationMetaResponse } from '@/types' + +export const deserializePaginationMeta = ( + meta: PaginationMetaResponse +): PaginationMeta => ({ + hasMore: meta.has_more, + currentPage: meta.current_page, + from: meta.from, + to: meta.to, + total: meta.total, + perPage: meta.per_page, + lastPage: meta.last_page, +})