diff --git a/src/core/http.ts b/src/core/http.ts index 9685aff..8b64c86 100644 --- a/src/core/http.ts +++ b/src/core/http.ts @@ -29,20 +29,34 @@ export class HttpHandler { this.#baseUrl = props.urlEndpoint ?? DefaultEndpoint } - async get(path: string, data?: unknown): Promise { + async get(path: string, data?: unknown): Promise { return this.#request('GET', path, data) } - async post(path: string, data?: unknown): Promise { + async post(path: string, data?: unknown): Promise { return this.#request('POST', path, data) } - async delete(path: string, data?: unknown): Promise { + async delete(path: string, data?: unknown): Promise { return this.#request('DELETE', path, data) } - async #request(method: HttpMethod, path: string, data?: unknown): Promise { - const url = `${this.#baseUrl}/client/${path}` + async #request(method: HttpMethod, path: string, data?: unknown): Promise { + let url = `${this.#baseUrl}/client/${path}` + let body: string | undefined + + if (method === 'GET' && data) { + const mapped = mapKeys(data) as Record + const params = new URLSearchParams() + for (const [key, value] of Object.entries(mapped)) { + if (value !== null && value !== undefined) { + params.set(key, String(value)) + } + } + url = `${url}?${params.toString()}` + } else if (data) { + body = JSON.stringify(mapKeys(data)) + } try { const response = await fetch(url, { @@ -51,7 +65,7 @@ export class HttpHandler { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.#apiKey}`, }, - body: data ? JSON.stringify(mapKeys(data)) : undefined, + body, }) return this.#handleResponse(response) @@ -65,17 +79,22 @@ export class HttpHandler { } } - async #handleResponse(response: Response): Promise { + async #handleResponse(response: Response): Promise { if (!response.ok) { throw await this.#mapError(response) } + const contentLength = response.headers.get('content-length') + if (contentLength === '0') { + return undefined + } + const contentType = response.headers.get('content-type') if (!contentType || !contentType.includes('application/json')) { - return undefined as T + return undefined } - return response.json() as Promise + return response.json() } async #mapError(response: Response): Promise { diff --git a/src/core/resources/base.ts b/src/core/resources/base.ts index 5f418bf..a8162e4 100644 --- a/src/core/resources/base.ts +++ b/src/core/resources/base.ts @@ -9,15 +9,15 @@ export abstract class BaseResource { protected abstract readonly endpoint: string - protected async get(data?: unknown): Promise { - return this.#http.get(this.endpoint, data) + protected async get(data?: unknown, pathOverride?: string): Promise { + return this.#http.get(pathOverride ?? this.endpoint, data) } - protected async post(data?: unknown, pathOverride?: string): Promise { + protected async post(data?: unknown, pathOverride?: string): Promise { return this.#http.post(pathOverride ?? this.endpoint, data) } - protected async remove(data?: unknown, pathOverride?: string): Promise { + protected async remove(data?: unknown, pathOverride?: string): Promise { return this.#http.delete(pathOverride ?? this.endpoint, data) } } diff --git a/src/core/resources/organizations/events.ts b/src/core/resources/organizations/events.ts index 8e7face..6a70c85 100644 --- a/src/core/resources/organizations/events.ts +++ b/src/core/resources/organizations/events.ts @@ -8,7 +8,7 @@ export class OrganizationEventsResource extends BaseResource { * Posts organization events for asynchronous processing. * @param data - Array of organization events */ - async post(data: OrganizationEvent[]): Promise { - return super.post(data) + async post(data: OrganizationEvent[]): Promise { + return super.post(data) } } diff --git a/src/core/resources/organizations/inbox.ts b/src/core/resources/organizations/inbox.ts new file mode 100644 index 0000000..aef8c0c --- /dev/null +++ b/src/core/resources/organizations/inbox.ts @@ -0,0 +1,48 @@ +import { BaseResource } from '../base' +import { + PostInboxMessagesRequest, + InboxMessageEvents, + GetInboxParams, + GetInboxCountParams, + InboxMessageList, + InboxCount, +} from '../../../types' + +export class OrganizationInboxResource extends BaseResource { + readonly endpoint = 'organizations/inbox' + + /** + * Creates one or more inbox messages for organizations. Processed asynchronously. + */ + async create(data: PostInboxMessagesRequest): Promise { + return super.post(data) + } + + /** + * Returns visible, non-expired inbox messages for an organization. + */ + async list(params: GetInboxParams): Promise { + return super.get(params) + } + + /** + * Returns unread and total inbox message counts for an organization. + */ + async count(params: GetInboxCountParams): Promise { + return super.get(params, 'organizations/inbox/count') + } + + /** + * Marks one or more inbox messages as opened. Processed asynchronously. + */ + async opened(data: InboxMessageEvents): Promise { + return super.post(data, 'organizations/inbox/opened') + } + + /** + * Marks one or more inbox messages as archived. Processed asynchronously. + */ + async archived(data: InboxMessageEvents): Promise { + return super.post(data, 'organizations/inbox/archived') + } +} diff --git a/src/core/resources/organizations/index.ts b/src/core/resources/organizations/index.ts index 237ff2c..50e521b 100644 --- a/src/core/resources/organizations/index.ts +++ b/src/core/resources/organizations/index.ts @@ -1,3 +1,4 @@ export { OrganizationResource } from './organization' export { OrganizationScheduledResource } from './scheduled' -export { OrganizationEventsResource } from './events' \ No newline at end of file +export { OrganizationEventsResource } from './events' +export { OrganizationInboxResource } from './inbox' \ No newline at end of file diff --git a/src/core/resources/organizations/organization.ts b/src/core/resources/organizations/organization.ts index 19b291a..e2d7d09 100644 --- a/src/core/resources/organizations/organization.ts +++ b/src/core/resources/organizations/organization.ts @@ -9,16 +9,19 @@ import { BaseResource } from "../base" import { HttpHandler } from "../../http" import { OrganizationScheduledResource } from "./scheduled" import { OrganizationEventsResource } from "./events" +import { OrganizationInboxResource } from "./inbox" export class OrganizationResource extends BaseResource { readonly endpoint = 'organizations' readonly schedule: OrganizationScheduledResource readonly events: OrganizationEventsResource + readonly inbox: OrganizationInboxResource constructor(http: HttpHandler) { super(http) this.schedule = new OrganizationScheduledResource(http) this.events = new OrganizationEventsResource(http) + this.inbox = new OrganizationInboxResource(http) } /** @@ -26,8 +29,8 @@ export class OrganizationResource extends BaseResource { * @param data - Organization data including identifier, name, data, etc. * @returns Promise resolving to the created/updated organization */ - async upsert(data: OrganizationRequest): Promise { - return this.post(data) + async upsert(data: OrganizationRequest): Promise { + return this.post(data) } /** @@ -36,7 +39,7 @@ export class OrganizationResource extends BaseResource { * @returns Promise resolving when organization is deleted */ async delete(data: DeleteOrganizationRequest): Promise { - return this.remove(data) + return this.remove(data) } /** @@ -45,7 +48,7 @@ export class OrganizationResource extends BaseResource { * @returns Promise resolving when user is added */ async addUser(data: OrganizationUserRequest): Promise { - return this.post(data, 'organizations/users') + return this.post(data, 'organizations/users') } /** @@ -54,6 +57,6 @@ export class OrganizationResource extends BaseResource { * @returns Promise resolving when user is removed */ async removeUser(data: RemoveOrganizationUserRequest): Promise { - return this.remove(data, 'organizations/users') + return this.remove(data, 'organizations/users') } } diff --git a/src/core/resources/organizations/scheduled.ts b/src/core/resources/organizations/scheduled.ts index af2f77d..97d7f1f 100644 --- a/src/core/resources/organizations/scheduled.ts +++ b/src/core/resources/organizations/scheduled.ts @@ -16,8 +16,8 @@ export class OrganizationScheduledResource extends BaseResource { * @param data - Scheduled resource data including name, identifier, scheduledAt, interval, etc. * @returns Promise resolving to the accepted scheduled resource */ - async upsert(data: UpsertOrganizationScheduledRequest): Promise { - return this.post(data) + async upsert(data: UpsertOrganizationScheduledRequest): Promise { + return this.post(data) } /** @@ -26,6 +26,6 @@ export class OrganizationScheduledResource extends BaseResource { * @returns Promise resolving when scheduled resource is deleted */ async delete(data: DeleteOrganizationScheduledRequest): Promise { - return this.remove(data) + return this.remove(data) } } diff --git a/src/core/resources/users/events.ts b/src/core/resources/users/events.ts index 39c3c48..31b0d65 100644 --- a/src/core/resources/users/events.ts +++ b/src/core/resources/users/events.ts @@ -8,7 +8,7 @@ export class UserEventsResource extends BaseResource { * Posts user events for asynchronous processing. * @param data - Array of user events */ - async post(data: UserEvent[]): Promise { - return super.post(data) + async post(data: UserEvent[]): Promise { + return super.post(data) } } diff --git a/src/core/resources/users/inbox.ts b/src/core/resources/users/inbox.ts new file mode 100644 index 0000000..0db15a4 --- /dev/null +++ b/src/core/resources/users/inbox.ts @@ -0,0 +1,48 @@ +import { BaseResource } from '../base' +import { + PostInboxMessagesRequest, + InboxMessageEvents, + GetInboxParams, + GetInboxCountParams, + InboxMessageList, + InboxCount, +} from '../../../types' + +export class UserInboxResource extends BaseResource { + readonly endpoint = 'users/inbox' + + /** + * Creates one or more inbox messages for users. Processed asynchronously. + */ + async create(data: PostInboxMessagesRequest): Promise { + return super.post(data) + } + + /** + * Returns visible, non-expired inbox messages for a user. + */ + async list(params: GetInboxParams): Promise { + return super.get(params) + } + + /** + * Returns unread and total inbox message counts for a user. + */ + async count(params: GetInboxCountParams): Promise { + return super.get(params, 'users/inbox/count') + } + + /** + * Marks one or more inbox messages as opened. Processed asynchronously. + */ + async opened(data: InboxMessageEvents): Promise { + return super.post(data, 'users/inbox/opened') + } + + /** + * Marks one or more inbox messages as archived. Processed asynchronously. + */ + async archived(data: InboxMessageEvents): Promise { + return super.post(data, 'users/inbox/archived') + } +} diff --git a/src/core/resources/users/index.ts b/src/core/resources/users/index.ts index aea3ac6..87b65fa 100644 --- a/src/core/resources/users/index.ts +++ b/src/core/resources/users/index.ts @@ -1,3 +1,4 @@ export { UserResource } from './user' export { UserScheduledResource } from './scheduled' -export { UserEventsResource } from './events' \ No newline at end of file +export { UserEventsResource } from './events' +export { UserInboxResource } from './inbox' \ No newline at end of file diff --git a/src/core/resources/users/scheduled.ts b/src/core/resources/users/scheduled.ts index bc033af..a25a597 100644 --- a/src/core/resources/users/scheduled.ts +++ b/src/core/resources/users/scheduled.ts @@ -16,8 +16,8 @@ export class UserScheduledResource extends BaseResource { * @param data - Scheduled resource data including name, identifier, scheduledAt, interval, etc. * @returns Promise resolving to the accepted scheduled resource */ - async upsert(data: UpsertUserScheduledRequest): Promise { - return this.post(data) + async upsert(data: UpsertUserScheduledRequest): Promise { + return this.post(data) } /** @@ -26,6 +26,6 @@ export class UserScheduledResource extends BaseResource { * @returns Promise resolving when scheduled resource is deleted */ async delete(data: DeleteUserScheduledRequest): Promise { - return this.remove(data) + return this.remove(data) } } diff --git a/src/core/resources/users/user.ts b/src/core/resources/users/user.ts index 5af36fb..dc3cfe5 100644 --- a/src/core/resources/users/user.ts +++ b/src/core/resources/users/user.ts @@ -7,16 +7,19 @@ import { } from '../../../types' import { UserScheduledResource } from './scheduled' import { UserEventsResource } from './events' +import { UserInboxResource } from './inbox' export class UserResource extends BaseResource { readonly endpoint = 'users' readonly schedule: UserScheduledResource readonly events: UserEventsResource + readonly inbox: UserInboxResource constructor(http: HttpHandler) { super(http) this.schedule = new UserScheduledResource(http) this.events = new UserEventsResource(http) + this.inbox = new UserInboxResource(http) } /** @@ -24,8 +27,8 @@ export class UserResource extends BaseResource { * @param data - User data including identifier, email, phone, etc. * @returns Promise resolving to the created/updated user */ - async upsert(data: UpsertUserRequest): Promise { - return this.post(data) + async upsert(data: UpsertUserRequest): Promise { + return this.post(data) } /** @@ -34,6 +37,6 @@ export class UserResource extends BaseResource { * @returns Promise resolving when user is deleted */ async delete(data: DeleteUserRequest): Promise { - return this.remove(data) + return this.remove(data) } } diff --git a/src/platform/browser.ts b/src/platform/browser.ts index 99e068c..91c11e0 100644 --- a/src/platform/browser.ts +++ b/src/platform/browser.ts @@ -24,14 +24,14 @@ class BrowserUserEventsResource extends UserEventsResource { this.#getIdentifier = getIdentifier } - async post(data: UserEvent[]): Promise { + async post(data: UserEvent[]): Promise { const identifier = this.#getIdentifier() const injected = data.map((event) => ({ ...event, // Do not inject identifier when `match` is used (they are mutually exclusive) identifier: event.match ? event.identifier : (event.identifier ?? identifier), })) - return super.post(injected) as Promise + return super.post(injected) } } @@ -43,7 +43,7 @@ class BrowserUserScheduledResource extends UserScheduledResource { this.#getIdentifier = getIdentifier } - async upsert(data: UpsertUserScheduledRequest): Promise { + async upsert(data: UpsertUserScheduledRequest): Promise { return super.upsert({ ...data, identifier: data.identifier ?? this.#getIdentifier(), @@ -113,7 +113,7 @@ class BrowserUserResource extends UserResource { return identifier } - async upsert(data: UpsertUserRequest): Promise { + async upsert(data: UpsertUserRequest): Promise { const identifier = this.#buildIdentifier(data.identifier) return super.upsert({ ...data, identifier }) } diff --git a/src/types/request.ts b/src/types/request.ts index ee5dd5f..c670506 100644 --- a/src/types/request.ts +++ b/src/types/request.ts @@ -116,6 +116,68 @@ export interface RemoveOrganizationUserRequest { } } +// Inbox types + +export type InboxStatus = 'unread' | 'opened' | 'archived' + +/** Request to create inbox messages */ +export interface PostInboxMessagesRequest { + source: string + externalId: string + messages: Record[] +} + +/** Request to mark inbox messages as opened or archived */ +export interface InboxMessageEvents { + source: string + externalId: string + messageIds: string[] +} + +/** Query params for fetching inbox messages */ +export interface GetInboxParams { + source: string + externalId: string + channel: string + status?: InboxStatus + tags?: string + messageSource?: string + priority?: number + limit?: number + offset?: number +} + +/** Query params for counting inbox messages */ +export interface GetInboxCountParams { + source: string + externalId: string + channel: string +} + +/** A single inbox message as returned by the API */ +export interface InboxMessage { + id: string + data: Record + status: InboxStatus + channel: string + priority: number + tags: string[] + createdAt: string + updatedAt: string +} + +/** Paginated list of inbox messages */ +export interface InboxMessageList { + data: InboxMessage[] + total: number +} + +/** Inbox message counts */ +export interface InboxCount { + unread: number + total: number +} + /** Event data for organization events */ export interface OrganizationEvent { /** Organization identifier array. Mutually exclusive with `match`. */