diff --git a/src/resources/actions.ts b/src/resources/actions.ts index 2dd8667..1e7ef92 100644 --- a/src/resources/actions.ts +++ b/src/resources/actions.ts @@ -61,11 +61,11 @@ export class ActionsResource { * Create a new action */ async create(data: ActionCreateData): Promise { - const response = await this.httpClient.request<{ actions: Action[] }>('/Actions', { + const response = await this.httpClient.request('/Actions', { method: 'POST', body: [data], }); - const action = response.actions[0]; + const action = unwrapSingle(response, 'actions'); if (!action) { throw new Error('Failed to create action'); } @@ -76,11 +76,11 @@ export class ActionsResource { * Update an existing action */ async update(id: number, data: ActionUpdateData): Promise { - const response = await this.httpClient.request<{ actions: Action[] }>('/Actions', { + const response = await this.httpClient.request('/Actions', { method: 'POST', body: [{ id, ...data }], }); - const action = response.actions[0]; + const action = unwrapSingle(response, 'actions'); if (!action) { throw new Error('Failed to update action'); } diff --git a/src/resources/agents.ts b/src/resources/agents.ts index 921663d..40b74ab 100644 --- a/src/resources/agents.ts +++ b/src/resources/agents.ts @@ -66,8 +66,8 @@ export class AgentsResource { * Get the current authenticated agent */ async me(): Promise { - const response = await this.httpClient.request<{ agents: Agent[] }>('/Agent/me'); - const agent = response.agents[0]; + const response = await this.httpClient.request('/Agent/me'); + const agent = unwrapSingle(response, 'agents'); if (!agent) { throw new Error('Failed to get current agent'); } @@ -78,11 +78,11 @@ export class AgentsResource { * Create a new agent */ async create(data: AgentCreateData): Promise { - const response = await this.httpClient.request<{ agents: Agent[] }>('/Agent', { + const response = await this.httpClient.request('/Agent', { method: 'POST', body: [data], }); - const agent = response.agents[0]; + const agent = unwrapSingle(response, 'agents'); if (!agent) { throw new Error('Failed to create agent'); } @@ -93,11 +93,11 @@ export class AgentsResource { * Update an existing agent */ async update(id: number, data: AgentUpdateData): Promise { - const response = await this.httpClient.request<{ agents: Agent[] }>('/Agent', { + const response = await this.httpClient.request('/Agent', { method: 'POST', body: [{ id, ...data }], }); - const agent = response.agents[0]; + const agent = unwrapSingle(response, 'agents'); if (!agent) { throw new Error('Failed to update agent'); } diff --git a/src/resources/appointments.ts b/src/resources/appointments.ts index 37e599b..0ff9197 100644 --- a/src/resources/appointments.ts +++ b/src/resources/appointments.ts @@ -66,11 +66,11 @@ export class AppointmentsResource { * Create a new appointment */ async create(data: AppointmentCreateData): Promise { - const response = await this.httpClient.request<{ appointments: Appointment[] }>('/Appointment', { + const response = await this.httpClient.request('/Appointment', { method: 'POST', body: [data], }); - const appointment = response.appointments[0]; + const appointment = unwrapSingle(response, 'appointments'); if (!appointment) { throw new Error('Failed to create appointment'); } @@ -81,11 +81,11 @@ export class AppointmentsResource { * Update an existing appointment */ async update(id: number, data: AppointmentUpdateData): Promise { - const response = await this.httpClient.request<{ appointments: Appointment[] }>('/Appointment', { + const response = await this.httpClient.request('/Appointment', { method: 'POST', body: [{ id, ...data }], }); - const appointment = response.appointments[0]; + const appointment = unwrapSingle(response, 'appointments'); if (!appointment) { throw new Error('Failed to update appointment'); } diff --git a/src/resources/assets.ts b/src/resources/assets.ts index 00794eb..238aff7 100644 --- a/src/resources/assets.ts +++ b/src/resources/assets.ts @@ -69,11 +69,11 @@ export class AssetsResource { * Create a new asset */ async create(data: AssetCreateData): Promise { - const response = await this.httpClient.request<{ assets: Asset[] }>('/Asset', { + const response = await this.httpClient.request('/Asset', { method: 'POST', body: [data], }); - const asset = response.assets[0]; + const asset = unwrapSingle(response, 'assets'); if (!asset) { throw new Error('Failed to create asset'); } @@ -84,11 +84,11 @@ export class AssetsResource { * Update an existing asset */ async update(id: number, data: AssetUpdateData): Promise { - const response = await this.httpClient.request<{ assets: Asset[] }>('/Asset', { + const response = await this.httpClient.request('/Asset', { method: 'POST', body: [{ id, ...data }], }); - const asset = response.assets[0]; + const asset = unwrapSingle(response, 'assets'); if (!asset) { throw new Error('Failed to update asset'); } diff --git a/src/resources/clients.ts b/src/resources/clients.ts index 9294286..108d9f3 100644 --- a/src/resources/clients.ts +++ b/src/resources/clients.ts @@ -66,11 +66,11 @@ export class ClientsResource { * Create a new client */ async create(data: ClientCreateData): Promise { - const response = await this.httpClient.request<{ clients: Client[] }>('/Client', { + const response = await this.httpClient.request('/Client', { method: 'POST', body: [data], }); - const client = response.clients[0]; + const client = unwrapSingle(response, 'clients'); if (!client) { throw new Error('Failed to create client'); } @@ -81,11 +81,11 @@ export class ClientsResource { * Update an existing client */ async update(id: number, data: ClientUpdateData): Promise { - const response = await this.httpClient.request<{ clients: Client[] }>('/Client', { + const response = await this.httpClient.request('/Client', { method: 'POST', body: [{ id, ...data }], }); - const client = response.clients[0]; + const client = unwrapSingle(response, 'clients'); if (!client) { throw new Error('Failed to update client'); } diff --git a/src/resources/contacts.ts b/src/resources/contacts.ts index 01a7000..90c6a14 100644 --- a/src/resources/contacts.ts +++ b/src/resources/contacts.ts @@ -66,11 +66,11 @@ export class ContactsResource { * Create a new contact */ async create(data: ContactCreateData): Promise { - const response = await this.httpClient.request<{ users: Contact[] }>('/Users', { + const response = await this.httpClient.request('/Users', { method: 'POST', body: [data], }); - const contact = response.users[0]; + const contact = unwrapSingle(response, 'users'); if (!contact) { throw new Error('Failed to create contact'); } @@ -81,11 +81,11 @@ export class ContactsResource { * Update an existing contact */ async update(id: number, data: ContactUpdateData): Promise { - const response = await this.httpClient.request<{ users: Contact[] }>('/Users', { + const response = await this.httpClient.request('/Users', { method: 'POST', body: [{ id, ...data }], }); - const contact = response.users[0]; + const contact = unwrapSingle(response, 'users'); if (!contact) { throw new Error('Failed to update contact'); } diff --git a/src/resources/contracts.ts b/src/resources/contracts.ts index 74a81c9..b101da3 100644 --- a/src/resources/contracts.ts +++ b/src/resources/contracts.ts @@ -66,11 +66,11 @@ export class ContractsResource { * Create a new contract */ async create(data: ContractCreateData): Promise { - const response = await this.httpClient.request<{ contracts: Contract[] }>('/ClientContract', { + const response = await this.httpClient.request('/ClientContract', { method: 'POST', body: [data], }); - const contract = response.contracts[0]; + const contract = unwrapSingle(response, 'contracts'); if (!contract) { throw new Error('Failed to create contract'); } @@ -81,11 +81,11 @@ export class ContractsResource { * Update an existing contract */ async update(id: number, data: ContractUpdateData): Promise { - const response = await this.httpClient.request<{ contracts: Contract[] }>('/ClientContract', { + const response = await this.httpClient.request('/ClientContract', { method: 'POST', body: [{ id, ...data }], }); - const contract = response.contracts[0]; + const contract = unwrapSingle(response, 'contracts'); if (!contract) { throw new Error('Failed to update contract'); } diff --git a/src/resources/invoices.ts b/src/resources/invoices.ts index 40a6876..f47df48 100644 --- a/src/resources/invoices.ts +++ b/src/resources/invoices.ts @@ -66,11 +66,11 @@ export class InvoicesResource { * Create a new invoice */ async create(data: InvoiceCreateData): Promise { - const response = await this.httpClient.request<{ invoices: Invoice[] }>('/Invoice', { + const response = await this.httpClient.request('/Invoice', { method: 'POST', body: [data], }); - const invoice = response.invoices[0]; + const invoice = unwrapSingle(response, 'invoices'); if (!invoice) { throw new Error('Failed to create invoice'); } @@ -81,11 +81,11 @@ export class InvoicesResource { * Update an existing invoice */ async update(id: number, data: InvoiceUpdateData): Promise { - const response = await this.httpClient.request<{ invoices: Invoice[] }>('/Invoice', { + const response = await this.httpClient.request('/Invoice', { method: 'POST', body: [{ id, ...data }], }); - const invoice = response.invoices[0]; + const invoice = unwrapSingle(response, 'invoices'); if (!invoice) { throw new Error('Failed to update invoice'); } diff --git a/src/resources/items.ts b/src/resources/items.ts index b47ec1b..84774ba 100644 --- a/src/resources/items.ts +++ b/src/resources/items.ts @@ -66,11 +66,11 @@ export class ItemsResource { * Create a new item */ async create(data: ItemCreateData): Promise { - const response = await this.httpClient.request<{ items: Item[] }>('/Item', { + const response = await this.httpClient.request('/Item', { method: 'POST', body: [data], }); - const item = response.items[0]; + const item = unwrapSingle(response, 'items'); if (!item) { throw new Error('Failed to create item'); } @@ -81,11 +81,11 @@ export class ItemsResource { * Update an existing item */ async update(id: number, data: ItemUpdateData): Promise { - const response = await this.httpClient.request<{ items: Item[] }>('/Item', { + const response = await this.httpClient.request('/Item', { method: 'POST', body: [{ id, ...data }], }); - const item = response.items[0]; + const item = unwrapSingle(response, 'items'); if (!item) { throw new Error('Failed to update item'); } diff --git a/src/resources/opportunities.ts b/src/resources/opportunities.ts index 499f367..d43925f 100644 --- a/src/resources/opportunities.ts +++ b/src/resources/opportunities.ts @@ -66,11 +66,11 @@ export class OpportunitiesResource { * Create a new opportunity */ async create(data: OpportunityCreateData): Promise { - const response = await this.httpClient.request<{ opportunities: Opportunity[] }>('/Opportunities', { + const response = await this.httpClient.request('/Opportunities', { method: 'POST', body: [data], }); - const opportunity = response.opportunities[0]; + const opportunity = unwrapSingle(response, 'opportunities'); if (!opportunity) { throw new Error('Failed to create opportunity'); } @@ -81,11 +81,11 @@ export class OpportunitiesResource { * Update an existing opportunity */ async update(id: number, data: OpportunityUpdateData): Promise { - const response = await this.httpClient.request<{ opportunities: Opportunity[] }>('/Opportunities', { + const response = await this.httpClient.request('/Opportunities', { method: 'POST', body: [{ id, ...data }], }); - const opportunity = response.opportunities[0]; + const opportunity = unwrapSingle(response, 'opportunities'); if (!opportunity) { throw new Error('Failed to update opportunity'); } diff --git a/src/resources/projects.ts b/src/resources/projects.ts index a4ec418..26ff515 100644 --- a/src/resources/projects.ts +++ b/src/resources/projects.ts @@ -68,11 +68,11 @@ export class ProjectsResource { * Create a new project */ async create(data: ProjectCreateData): Promise { - const response = await this.httpClient.request<{ projects: Project[] }>('/Projects', { + const response = await this.httpClient.request('/Projects', { method: 'POST', body: [data], }); - const project = response.projects[0]; + const project = unwrapSingle(response, 'projects'); if (!project) { throw new Error('Failed to create project'); } @@ -83,11 +83,11 @@ export class ProjectsResource { * Update an existing project */ async update(id: number, data: ProjectUpdateData): Promise { - const response = await this.httpClient.request<{ projects: Project[] }>('/Projects', { + const response = await this.httpClient.request('/Projects', { method: 'POST', body: [{ id, ...data }], }); - const project = response.projects[0]; + const project = unwrapSingle(response, 'projects'); if (!project) { throw new Error('Failed to update project'); } diff --git a/src/resources/quotes.ts b/src/resources/quotes.ts index 8ea914b..f49ca4e 100644 --- a/src/resources/quotes.ts +++ b/src/resources/quotes.ts @@ -67,11 +67,11 @@ export class QuotesResource { * Create a new quote */ async create(data: QuoteCreateData): Promise { - const response = await this.httpClient.request<{ quotations: Quote[] }>('/Quotation', { + const response = await this.httpClient.request('/Quotation', { method: 'POST', body: [data], }); - const quote = response.quotations[0]; + const quote = unwrapSingle(response, 'quotations'); if (!quote) { throw new Error('Failed to create quote'); } @@ -82,11 +82,11 @@ export class QuotesResource { * Update an existing quote */ async update(id: number, data: QuoteUpdateData): Promise { - const response = await this.httpClient.request<{ quotations: Quote[] }>('/Quotation', { + const response = await this.httpClient.request('/Quotation', { method: 'POST', body: [{ id, ...data }], }); - const quote = response.quotations[0]; + const quote = unwrapSingle(response, 'quotations'); if (!quote) { throw new Error('Failed to update quote'); } diff --git a/src/resources/reference.ts b/src/resources/reference.ts index 47ace27..7f413eb 100644 --- a/src/resources/reference.ts +++ b/src/resources/reference.ts @@ -224,21 +224,21 @@ export class KnowledgeBaseResource { } async create(data: KBArticleCreateData): Promise { - const response = await this.httpClient.request<{ articles: KBArticle[] }>('/KBArticle', { + const response = await this.httpClient.request('/KBArticle', { method: 'POST', body: [data], }); - const article = response.articles[0]; + const article = unwrapSingle(response, 'articles'); if (!article) throw new Error('Failed to create KB article'); return article; } async update(id: number, data: KBArticleUpdateData): Promise { - const response = await this.httpClient.request<{ articles: KBArticle[] }>('/KBArticle', { + const response = await this.httpClient.request('/KBArticle', { method: 'POST', body: [{ id, ...data }], }); - const article = response.articles[0]; + const article = unwrapSingle(response, 'articles'); if (!article) throw new Error('Failed to update KB article'); return article; } @@ -269,21 +269,21 @@ export class RecurringInvoicesResource { } async create(data: RecurringInvoiceCreateData): Promise { - const response = await this.httpClient.request<{ recurring_invoices: RecurringInvoice[] }>('/RecurringInvoice', { + const response = await this.httpClient.request('/RecurringInvoice', { method: 'POST', body: [data], }); - const invoice = response.recurring_invoices[0]; + const invoice = unwrapSingle(response, 'recurring_invoices'); if (!invoice) throw new Error('Failed to create recurring invoice'); return invoice; } async update(id: number, data: RecurringInvoiceUpdateData): Promise { - const response = await this.httpClient.request<{ recurring_invoices: RecurringInvoice[] }>('/RecurringInvoice', { + const response = await this.httpClient.request('/RecurringInvoice', { method: 'POST', body: [{ id, ...data }], }); - const invoice = response.recurring_invoices[0]; + const invoice = unwrapSingle(response, 'recurring_invoices'); if (!invoice) throw new Error('Failed to update recurring invoice'); return invoice; } @@ -342,21 +342,21 @@ export class SoftwareLicencesResource { } async create(data: SoftwareLicenceCreateData): Promise { - const response = await this.httpClient.request<{ software_licences: SoftwareLicence[] }>('/SoftwareLicence', { + const response = await this.httpClient.request('/SoftwareLicence', { method: 'POST', body: [data], }); - const licence = response.software_licences[0]; + const licence = unwrapSingle(response, 'software_licences'); if (!licence) throw new Error('Failed to create software licence'); return licence; } async update(id: number, data: SoftwareLicenceUpdateData): Promise { - const response = await this.httpClient.request<{ software_licences: SoftwareLicence[] }>('/SoftwareLicence', { + const response = await this.httpClient.request('/SoftwareLicence', { method: 'POST', body: [{ id, ...data }], }); - const licence = response.software_licences[0]; + const licence = unwrapSingle(response, 'software_licences'); if (!licence) throw new Error('Failed to update software licence'); return licence; } diff --git a/src/resources/sites.ts b/src/resources/sites.ts index d9f9599..b4eebba 100644 --- a/src/resources/sites.ts +++ b/src/resources/sites.ts @@ -66,11 +66,11 @@ export class SitesResource { * Create a new site */ async create(data: SiteCreateData): Promise { - const response = await this.httpClient.request<{ sites: Site[] }>('/Site', { + const response = await this.httpClient.request('/Site', { method: 'POST', body: [data], }); - const site = response.sites[0]; + const site = unwrapSingle(response, 'sites'); if (!site) { throw new Error('Failed to create site'); } @@ -81,11 +81,11 @@ export class SitesResource { * Update an existing site */ async update(id: number, data: SiteUpdateData): Promise { - const response = await this.httpClient.request<{ sites: Site[] }>('/Site', { + const response = await this.httpClient.request('/Site', { method: 'POST', body: [{ id, ...data }], }); - const site = response.sites[0]; + const site = unwrapSingle(response, 'sites'); if (!site) { throw new Error('Failed to update site'); } diff --git a/src/resources/suppliers.ts b/src/resources/suppliers.ts index 3f96ba3..272b38c 100644 --- a/src/resources/suppliers.ts +++ b/src/resources/suppliers.ts @@ -66,11 +66,11 @@ export class SuppliersResource { * Create a new supplier */ async create(data: SupplierCreateData): Promise { - const response = await this.httpClient.request<{ suppliers: Supplier[] }>('/Supplier', { + const response = await this.httpClient.request('/Supplier', { method: 'POST', body: [data], }); - const supplier = response.suppliers[0]; + const supplier = unwrapSingle(response, 'suppliers'); if (!supplier) { throw new Error('Failed to create supplier'); } @@ -81,11 +81,11 @@ export class SuppliersResource { * Update an existing supplier */ async update(id: number, data: SupplierUpdateData): Promise { - const response = await this.httpClient.request<{ suppliers: Supplier[] }>('/Supplier', { + const response = await this.httpClient.request('/Supplier', { method: 'POST', body: [{ id, ...data }], }); - const supplier = response.suppliers[0]; + const supplier = unwrapSingle(response, 'suppliers'); if (!supplier) { throw new Error('Failed to update supplier'); } diff --git a/src/resources/teams.ts b/src/resources/teams.ts index 98b30ec..e0e76a8 100644 --- a/src/resources/teams.ts +++ b/src/resources/teams.ts @@ -66,11 +66,11 @@ export class TeamsResource { * Create a new team */ async create(data: TeamCreateData): Promise { - const response = await this.httpClient.request<{ teams: Team[] }>('/Team', { + const response = await this.httpClient.request('/Team', { method: 'POST', body: [data], }); - const team = response.teams[0]; + const team = unwrapSingle(response, 'teams'); if (!team) { throw new Error('Failed to create team'); } @@ -81,11 +81,11 @@ export class TeamsResource { * Update an existing team */ async update(id: number, data: TeamUpdateData): Promise { - const response = await this.httpClient.request<{ teams: Team[] }>('/Team', { + const response = await this.httpClient.request('/Team', { method: 'POST', body: [{ id, ...data }], }); - const team = response.teams[0]; + const team = unwrapSingle(response, 'teams'); if (!team) { throw new Error('Failed to update team'); } diff --git a/src/resources/tickets.ts b/src/resources/tickets.ts index d9c6113..b5644d8 100644 --- a/src/resources/tickets.ts +++ b/src/resources/tickets.ts @@ -68,11 +68,11 @@ export class TicketsResource { * Create a new ticket */ async create(data: TicketCreateData): Promise { - const response = await this.httpClient.request<{ tickets: Ticket[] }>('/Tickets', { + const response = await this.httpClient.request('/Tickets', { method: 'POST', body: [data], }); - const ticket = response.tickets[0]; + const ticket = unwrapSingle(response, 'tickets'); if (!ticket) { throw new Error('Failed to create ticket'); } @@ -83,11 +83,11 @@ export class TicketsResource { * Update an existing ticket */ async update(id: number, data: TicketUpdateData): Promise { - const response = await this.httpClient.request<{ tickets: Ticket[] }>('/Tickets', { + const response = await this.httpClient.request('/Tickets', { method: 'POST', body: [{ id, ...data }], }); - const ticket = response.tickets[0]; + const ticket = unwrapSingle(response, 'tickets'); if (!ticket) { throw new Error('Failed to update ticket'); } @@ -116,11 +116,11 @@ export class TicketsResource { * Add an action to a ticket */ async addAction(id: number, data: ActionCreateData): Promise { - const response = await this.httpClient.request<{ actions: TicketAction[] }>('/Actions', { + const response = await this.httpClient.request('/Actions', { method: 'POST', body: [{ ticket_id: id, ...data }], }); - const action = response.actions[0]; + const action = unwrapSingle(response, 'actions'); if (!action) { throw new Error('Failed to create action'); } @@ -138,11 +138,11 @@ export class TicketsResource { * Add an attachment to a ticket */ async addAttachment(id: number, data: AttachmentCreateData): Promise { - const response = await this.httpClient.request<{ attachments: TicketAttachment[] }>(`/Tickets/${id}/Attachments`, { + const response = await this.httpClient.request(`/Tickets/${id}/Attachments`, { method: 'POST', body: [data], }); - const attachment = response.attachments[0]; + const attachment = unwrapSingle(response, 'attachments'); if (!attachment) { throw new Error('Failed to create attachment'); } diff --git a/src/resources/utils.ts b/src/resources/utils.ts index abcc607..cb40ccc 100644 --- a/src/resources/utils.ts +++ b/src/resources/utils.ts @@ -3,18 +3,22 @@ */ /** - * HaloPSA's `GET //{id}` endpoints sometimes return the entity bare - * (`{ id: 1, ... }`) and sometimes wrap it in a list-style envelope - * (`{ entities: [{...}] }`). The shape is endpoint- and version-dependent. - * This helper accepts either form and returns the entity (or undefined). + * HaloPSA's single-entity endpoints (`GET //{id}` and `POST /` + * create/update) return the entity bare (`{ id: 1, ... }`), wrapped in a + * list-style envelope (`{ entities: [{...}] }`), or as a bare array + * (`[{ id: 1, ... }]`). The shape is endpoint- and version-dependent. + * This helper accepts all three forms and returns the entity (or undefined). */ export function unwrapSingle( - response: T | Record | undefined | null, + response: T | T[] | Record | undefined | null, listKey: string ): T | undefined { if (!response || typeof response !== 'object') { return undefined; } + if (Array.isArray(response)) { + return response[0] as T | undefined; + } const wrapped = (response as Record)[listKey]; if (Array.isArray(wrapped)) { return wrapped[0] as T | undefined; diff --git a/tests/unit/resources.test.ts b/tests/unit/resources.test.ts new file mode 100644 index 0000000..979dec2 --- /dev/null +++ b/tests/unit/resources.test.ts @@ -0,0 +1,109 @@ +/** + * Resource write-path response shape tests. + * + * HaloPSA returns created/updated entities in three shapes depending on + * endpoint and version: enveloped ({ actions: [{...}] }), bare object + * ({ id: 1, ... }), or bare array ([{ id: 1, ... }]). Every create/update + * path must accept all three — see halopsa-mcp#76. + */ + +import { describe, it, expect, vi } from 'vitest'; +import type { HttpClient } from '../../src/http.js'; +import { ActionsResource } from '../../src/resources/actions.js'; +import { AgentsResource } from '../../src/resources/agents.js'; +import { AppointmentsResource } from '../../src/resources/appointments.js'; +import { AssetsResource } from '../../src/resources/assets.js'; +import { ClientsResource } from '../../src/resources/clients.js'; +import { ContactsResource } from '../../src/resources/contacts.js'; +import { ContractsResource } from '../../src/resources/contracts.js'; +import { InvoicesResource } from '../../src/resources/invoices.js'; +import { ItemsResource } from '../../src/resources/items.js'; +import { OpportunitiesResource } from '../../src/resources/opportunities.js'; +import { ProjectsResource } from '../../src/resources/projects.js'; +import { QuotesResource } from '../../src/resources/quotes.js'; +import { + KnowledgeBaseResource, + RecurringInvoicesResource, + SoftwareLicencesResource, +} from '../../src/resources/reference.js'; +import { SitesResource } from '../../src/resources/sites.js'; +import { SuppliersResource } from '../../src/resources/suppliers.js'; +import { TeamsResource } from '../../src/resources/teams.js'; +import { TicketsResource } from '../../src/resources/tickets.js'; + +const entity = { id: 42, name: 'test-entity' }; +const data = {} as never; + +const mockHttpClient = (response: unknown): HttpClient => + ({ request: vi.fn().mockResolvedValue(response) }) as unknown as HttpClient; + +interface WriteCase { + name: string; + listKey: string; + invoke: (httpClient: HttpClient) => Promise; +} + +const cases: WriteCase[] = [ + { name: 'actions.create', listKey: 'actions', invoke: (hc) => new ActionsResource(hc).create(data) }, + { name: 'actions.update', listKey: 'actions', invoke: (hc) => new ActionsResource(hc).update(1, data) }, + { name: 'agents.me', listKey: 'agents', invoke: (hc) => new AgentsResource(hc).me() }, + { name: 'agents.create', listKey: 'agents', invoke: (hc) => new AgentsResource(hc).create(data) }, + { name: 'agents.update', listKey: 'agents', invoke: (hc) => new AgentsResource(hc).update(1, data) }, + { name: 'appointments.create', listKey: 'appointments', invoke: (hc) => new AppointmentsResource(hc).create(data) }, + { name: 'appointments.update', listKey: 'appointments', invoke: (hc) => new AppointmentsResource(hc).update(1, data) }, + { name: 'assets.create', listKey: 'assets', invoke: (hc) => new AssetsResource(hc).create(data) }, + { name: 'assets.update', listKey: 'assets', invoke: (hc) => new AssetsResource(hc).update(1, data) }, + { name: 'clients.create', listKey: 'clients', invoke: (hc) => new ClientsResource(hc).create(data) }, + { name: 'clients.update', listKey: 'clients', invoke: (hc) => new ClientsResource(hc).update(1, data) }, + { name: 'contacts.create', listKey: 'users', invoke: (hc) => new ContactsResource(hc).create(data) }, + { name: 'contacts.update', listKey: 'users', invoke: (hc) => new ContactsResource(hc).update(1, data) }, + { name: 'contracts.create', listKey: 'contracts', invoke: (hc) => new ContractsResource(hc).create(data) }, + { name: 'contracts.update', listKey: 'contracts', invoke: (hc) => new ContractsResource(hc).update(1, data) }, + { name: 'invoices.create', listKey: 'invoices', invoke: (hc) => new InvoicesResource(hc).create(data) }, + { name: 'invoices.update', listKey: 'invoices', invoke: (hc) => new InvoicesResource(hc).update(1, data) }, + { name: 'items.create', listKey: 'items', invoke: (hc) => new ItemsResource(hc).create(data) }, + { name: 'items.update', listKey: 'items', invoke: (hc) => new ItemsResource(hc).update(1, data) }, + { name: 'opportunities.create', listKey: 'opportunities', invoke: (hc) => new OpportunitiesResource(hc).create(data) }, + { name: 'opportunities.update', listKey: 'opportunities', invoke: (hc) => new OpportunitiesResource(hc).update(1, data) }, + { name: 'projects.create', listKey: 'projects', invoke: (hc) => new ProjectsResource(hc).create(data) }, + { name: 'projects.update', listKey: 'projects', invoke: (hc) => new ProjectsResource(hc).update(1, data) }, + { name: 'quotes.create', listKey: 'quotations', invoke: (hc) => new QuotesResource(hc).create(data) }, + { name: 'quotes.update', listKey: 'quotations', invoke: (hc) => new QuotesResource(hc).update(1, data) }, + { name: 'knowledgeBase.create', listKey: 'articles', invoke: (hc) => new KnowledgeBaseResource(hc).create(data) }, + { name: 'knowledgeBase.update', listKey: 'articles', invoke: (hc) => new KnowledgeBaseResource(hc).update(1, data) }, + { name: 'recurringInvoices.create', listKey: 'recurring_invoices', invoke: (hc) => new RecurringInvoicesResource(hc).create(data) }, + { name: 'recurringInvoices.update', listKey: 'recurring_invoices', invoke: (hc) => new RecurringInvoicesResource(hc).update(1, data) }, + { name: 'softwareLicences.create', listKey: 'software_licences', invoke: (hc) => new SoftwareLicencesResource(hc).create(data) }, + { name: 'softwareLicences.update', listKey: 'software_licences', invoke: (hc) => new SoftwareLicencesResource(hc).update(1, data) }, + { name: 'sites.create', listKey: 'sites', invoke: (hc) => new SitesResource(hc).create(data) }, + { name: 'sites.update', listKey: 'sites', invoke: (hc) => new SitesResource(hc).update(1, data) }, + { name: 'suppliers.create', listKey: 'suppliers', invoke: (hc) => new SuppliersResource(hc).create(data) }, + { name: 'suppliers.update', listKey: 'suppliers', invoke: (hc) => new SuppliersResource(hc).update(1, data) }, + { name: 'teams.create', listKey: 'teams', invoke: (hc) => new TeamsResource(hc).create(data) }, + { name: 'teams.update', listKey: 'teams', invoke: (hc) => new TeamsResource(hc).update(1, data) }, + { name: 'tickets.create', listKey: 'tickets', invoke: (hc) => new TicketsResource(hc).create(data) }, + { name: 'tickets.update', listKey: 'tickets', invoke: (hc) => new TicketsResource(hc).update(1, data) }, + { name: 'tickets.addAction', listKey: 'actions', invoke: (hc) => new TicketsResource(hc).addAction(1, data) }, + { name: 'tickets.addAttachment', listKey: 'attachments', invoke: (hc) => new TicketsResource(hc).addAttachment(1, data) }, +]; + +describe.each(cases)('$name', ({ listKey, invoke }) => { + it('unwraps an enveloped response', async () => { + const result = await invoke(mockHttpClient({ [listKey]: [entity] })); + expect(result).toEqual(entity); + }); + + it('accepts a bare object response', async () => { + const result = await invoke(mockHttpClient(entity)); + expect(result).toEqual(entity); + }); + + it('accepts a bare array response', async () => { + const result = await invoke(mockHttpClient([entity])); + expect(result).toEqual(entity); + }); + + it('throws when the response contains no entity', async () => { + await expect(invoke(mockHttpClient({ [listKey]: [] }))).rejects.toThrow(); + }); +}); diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts index 3c64304..37bd33f 100644 --- a/tests/unit/utils.test.ts +++ b/tests/unit/utils.test.ts @@ -16,10 +16,19 @@ describe('unwrapSingle', () => { expect(unwrapSingle<{ id: number }>(bare, 'tickets')).toEqual(bare); }); + it('returns first element of bare array response', () => { + const bareArray = [{ id: 4, summary: 'bare array' }, { id: 5, summary: 'second' }]; + expect(unwrapSingle<{ id: number }>(bareArray, 'tickets')).toEqual({ id: 4, summary: 'bare array' }); + }); + it('returns undefined when wrapped list is empty', () => { expect(unwrapSingle({ tickets: [] }, 'tickets')).toBeUndefined(); }); + it('returns undefined when bare array is empty', () => { + expect(unwrapSingle([], 'tickets')).toBeUndefined(); + }); + it('returns undefined for null/undefined response', () => { expect(unwrapSingle(null, 'tickets')).toBeUndefined(); expect(unwrapSingle(undefined, 'tickets')).toBeUndefined();