@ikatec/digisac-api-sdk 4.3.0 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apis/contacts/ContactsApi.cjs +70 -0
- package/dist/apis/contacts/ContactsApi.cjs.map +1 -1
- package/dist/apis/contacts/ContactsApi.d.ts +51 -1
- package/dist/apis/contacts/ContactsApi.d.ts.map +1 -1
- package/dist/apis/contacts/ContactsApi.mjs +70 -0
- package/dist/apis/contacts/ContactsApi.mjs.map +1 -1
- package/dist/apis/contacts/types.d.ts +141 -1
- package/dist/apis/contacts/types.d.ts.map +1 -1
- package/dist/apis/messages/MessagesApi.cjs +4 -1
- package/dist/apis/messages/MessagesApi.cjs.map +1 -1
- package/dist/apis/messages/MessagesApi.d.ts +5 -2
- package/dist/apis/messages/MessagesApi.d.ts.map +1 -1
- package/dist/apis/messages/MessagesApi.mjs +4 -1
- package/dist/apis/messages/MessagesApi.mjs.map +1 -1
- package/dist/apis/messages/types.d.ts +13 -0
- package/dist/apis/messages/types.d.ts.map +1 -1
- package/dist/apis/services/ServicesApi.cjs +8 -0
- package/dist/apis/services/ServicesApi.cjs.map +1 -1
- package/dist/apis/services/ServicesApi.d.ts +7 -1
- package/dist/apis/services/ServicesApi.d.ts.map +1 -1
- package/dist/apis/services/ServicesApi.mjs +8 -0
- package/dist/apis/services/ServicesApi.mjs.map +1 -1
- package/dist/apis/services/types.d.ts +7 -0
- package/dist/apis/services/types.d.ts.map +1 -1
- package/dist/apis/tags/TagsApi.cjs +24 -0
- package/dist/apis/tags/TagsApi.cjs.map +1 -1
- package/dist/apis/tags/TagsApi.d.ts +20 -1
- package/dist/apis/tags/TagsApi.d.ts.map +1 -1
- package/dist/apis/tags/TagsApi.mjs +24 -0
- package/dist/apis/tags/TagsApi.mjs.map +1 -1
- package/dist/apis/tags/types.d.ts +13 -0
- package/dist/apis/tags/types.d.ts.map +1 -1
- package/dist/apis/users/UsersApi.cjs +77 -0
- package/dist/apis/users/UsersApi.cjs.map +1 -1
- package/dist/apis/users/UsersApi.d.ts +59 -1
- package/dist/apis/users/UsersApi.d.ts.map +1 -1
- package/dist/apis/users/UsersApi.mjs +77 -0
- package/dist/apis/users/UsersApi.mjs.map +1 -1
- package/dist/apis/users/types.d.ts +123 -3
- package/dist/apis/users/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/apis/acceptanceTerms/AcceptanceTermsApi.test.ts +1 -1
- package/src/apis/authHistory/AuthHistoryApi.test.ts +1 -1
- package/src/apis/campaigns/CampaignsApi.test.ts +1 -1
- package/src/apis/contacts/ContactsApi.test.ts +182 -5
- package/src/apis/contacts/ContactsApi.ts +128 -0
- package/src/apis/contacts/types.ts +154 -1
- package/src/apis/me/MeApi.test.ts +3 -3
- package/src/apis/messages/MessagesApi.test.ts +14 -3
- package/src/apis/messages/MessagesApi.ts +6 -3
- package/src/apis/messages/types.ts +13 -0
- package/src/apis/payloads.test-d.ts +26 -0
- package/src/apis/queryFormat.test.ts +27 -3
- package/src/apis/serviceAccessManagement/ServiceAccessManagementApi.test.ts +2 -2
- package/src/apis/services/ServicesApi.test.ts +49 -0
- package/src/apis/services/ServicesApi.ts +10 -1
- package/src/apis/services/types.ts +9 -0
- package/src/apis/stickerUsers/StickerUsersApi.test.ts +1 -1
- package/src/apis/tags/TagsApi.test.ts +84 -0
- package/src/apis/tags/TagsApi.ts +59 -1
- package/src/apis/tags/types.ts +15 -0
- package/src/apis/terms/TermsApi.test.ts +1 -1
- package/src/apis/users/UsersApi.test.ts +228 -0
- package/src/apis/users/UsersApi.ts +152 -1
- package/src/apis/users/types.ts +144 -3
|
@@ -4,7 +4,7 @@ import type { Service } from '../services/types'
|
|
|
4
4
|
import type { Tag } from '../tags/types'
|
|
5
5
|
import type { Ticket } from '../tickets/types'
|
|
6
6
|
import type { User } from '../users/types'
|
|
7
|
-
import type { ListQuery } from '../../core/types'
|
|
7
|
+
import type { IncludeItem, ListQuery, WhereClause } from '../../core/types'
|
|
8
8
|
|
|
9
9
|
export type ContactStatus = 'online' | 'offline' | 'absent'
|
|
10
10
|
export type ContactOrigin = 'api' | 'app' | 'campaign' | 'driver' | 'import' | 'web'
|
|
@@ -204,3 +204,156 @@ export type TransferTicketPayload = {
|
|
|
204
204
|
kanbanCardSubject?: string
|
|
205
205
|
initialMessage?: string
|
|
206
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* One item of `POST /contacts/many`. The backend matches existing contacts by service and
|
|
210
|
+
* number (or e-mail) and updates them; the others are created. Items repeating a number of
|
|
211
|
+
* the same batch are dropped (the first one wins).
|
|
212
|
+
*/
|
|
213
|
+
export type CreateManyContactsItem = {
|
|
214
|
+
serviceId: string
|
|
215
|
+
name: string
|
|
216
|
+
/** Phone number with country code, or the e-mail address for e-mail connections. */
|
|
217
|
+
number: string
|
|
218
|
+
internalName?: string
|
|
219
|
+
personId?: string
|
|
220
|
+
defaultDepartmentId?: string
|
|
221
|
+
defaultUserId?: string
|
|
222
|
+
tagIds?: string[]
|
|
223
|
+
/** When `true`, the given `tagIds` are merged into the existing ones instead of replacing them. */
|
|
224
|
+
mergeRelations?: boolean
|
|
225
|
+
/**
|
|
226
|
+
* Only `data.number` is read, as a fallback when `number` is empty; the backend rebuilds
|
|
227
|
+
* `data` from the number.
|
|
228
|
+
*/
|
|
229
|
+
data?: {
|
|
230
|
+
number: string
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Selects a set of contacts by filter. Used by `DELETE /contacts/many` and by the tag
|
|
236
|
+
* attach/detach routes. The backend always scopes the filter to the current account.
|
|
237
|
+
*
|
|
238
|
+
* `where` is required on purpose: the backend applies the operation to every contact the
|
|
239
|
+
* filter matches, so an empty filter would reach every contact of the account.
|
|
240
|
+
*/
|
|
241
|
+
export type ContactsSelection = {
|
|
242
|
+
where: WhereClause<Contact>
|
|
243
|
+
include?: IncludeItem<Contact>[]
|
|
244
|
+
customFilter?: Record<string, unknown>
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* CSV layout of the contact export and import, as in `exportTemplate`:
|
|
249
|
+
* - `'colon'`: `;` as the column delimiter (any other value uses `,`);
|
|
250
|
+
* - `'google'`: phone columns as DDI + Phone ("Telefone") instead of DDI + Number.
|
|
251
|
+
*
|
|
252
|
+
* E-mail and webchat connections ignore it for the contact columns (Email / Email + Number).
|
|
253
|
+
*/
|
|
254
|
+
export type ContactsCsvType = 'colon' | 'google' | (string & {})
|
|
255
|
+
|
|
256
|
+
/** Body for `POST /contacts/export/csv`. */
|
|
257
|
+
export type ExportCsvPayload = Omit<ListQuery<Contact>, 'page' | 'perPage'> & {
|
|
258
|
+
serviceType: string
|
|
259
|
+
type: ContactsCsvType
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Body for `POST /contacts/import-contacts`. The CSV must be uploaded first (see `FilesApi`);
|
|
264
|
+
* the header line is checked against the template of the service type and language.
|
|
265
|
+
*/
|
|
266
|
+
export type ImportContactsPayload = {
|
|
267
|
+
service: Pick<Service, 'id' | 'accountId' | 'type'>
|
|
268
|
+
file: {
|
|
269
|
+
id: string
|
|
270
|
+
name: string
|
|
271
|
+
mimetype: string
|
|
272
|
+
/** Sent by the web app; the backend reads only `id`. */
|
|
273
|
+
urlToUpload?: string
|
|
274
|
+
}
|
|
275
|
+
csvHeaderLine: string[]
|
|
276
|
+
type: ContactsCsvType
|
|
277
|
+
tagsIds?: string[]
|
|
278
|
+
defaultDepartmentId?: string
|
|
279
|
+
defaultUserId?: string
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Body for `POST /contacts/:id/block`. Blocking a contact closes its open ticket. */
|
|
283
|
+
export type BlockContactPayload = {
|
|
284
|
+
block: boolean
|
|
285
|
+
byUserId?: string
|
|
286
|
+
description?: string
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Body for `POST /contacts/:id/sync`. */
|
|
290
|
+
export type SyncContactPayload = {
|
|
291
|
+
forceSync?: boolean
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export type ContactMediaType = 'media' | 'document' | 'link'
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Query for `GET /contacts/:id/media`. Cursor-based: pass the previous page's `nextCursor`
|
|
298
|
+
* as `where.cursor`.
|
|
299
|
+
*/
|
|
300
|
+
export type ContactMediaQuery = {
|
|
301
|
+
where?: {
|
|
302
|
+
/** `'media'` = images and videos. Omitted = images, videos and documents. */
|
|
303
|
+
type?: ContactMediaType
|
|
304
|
+
/** Case-insensitive filter by file name (ignored for links). */
|
|
305
|
+
name?: string
|
|
306
|
+
cursor?: string
|
|
307
|
+
}
|
|
308
|
+
perPage?: number
|
|
309
|
+
order?: [string, 'ASC' | 'DESC' | 'asc' | 'desc'][]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* One item of `GET /contacts/:id/media`. When the user cannot see the message, the
|
|
314
|
+
* identifying fields come as `'UNAUTHORIZED_MESSAGE'` and `isAuthorized` is `false`.
|
|
315
|
+
*/
|
|
316
|
+
export type ContactMediaItem = {
|
|
317
|
+
id: string
|
|
318
|
+
/** ID of the message that carries the file or link. */
|
|
319
|
+
attachedId: string
|
|
320
|
+
/** File name; absent for links. */
|
|
321
|
+
name?: string | null
|
|
322
|
+
/** Only for files. */
|
|
323
|
+
mimetype?: string
|
|
324
|
+
createdAt: string
|
|
325
|
+
updatedAt: string
|
|
326
|
+
accountId: string
|
|
327
|
+
url: string | null
|
|
328
|
+
/** Only for files (filled for videos). */
|
|
329
|
+
previewUrl?: string | null
|
|
330
|
+
isAuthorized: boolean
|
|
331
|
+
/** Only for `type: 'link'`. */
|
|
332
|
+
links?: string[]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export type ContactMediaPage = {
|
|
336
|
+
data: ContactMediaItem[]
|
|
337
|
+
limit: number
|
|
338
|
+
hasNextPage: boolean
|
|
339
|
+
nextCursor: string | null
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Body for `POST /contacts/ticket/bulk-transfer`. */
|
|
343
|
+
export type BulkTransferTicketsPayload = {
|
|
344
|
+
departmentId?: string
|
|
345
|
+
/** `null` removes the assigned user. */
|
|
346
|
+
userId?: string | null
|
|
347
|
+
comments?: string
|
|
348
|
+
/** IDs of the tickets to transfer. Ignored when `allContactsSelected` is `true`. */
|
|
349
|
+
ticketsSelectedId?: string[]
|
|
350
|
+
allContactsSelected?: boolean
|
|
351
|
+
/** Contact filter used with `allContactsSelected`. */
|
|
352
|
+
query?: ListQuery<Contact>
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export type BulkTransferTicketsResult = {
|
|
356
|
+
message: string
|
|
357
|
+
/** IDs of the tickets actually transferred (the ones the target user can access). */
|
|
358
|
+
ticketsSelectedId: string[]
|
|
359
|
+
}
|
|
@@ -34,15 +34,15 @@ const user: User = {
|
|
|
34
34
|
email: 'alice@example.com',
|
|
35
35
|
phoneNumber: null,
|
|
36
36
|
branch: null,
|
|
37
|
-
isSuperAdmin: false,
|
|
38
37
|
isClientUser: false,
|
|
39
|
-
active: true,
|
|
40
38
|
language: 'en',
|
|
41
39
|
isFirstLogin: false,
|
|
42
40
|
status: 'online',
|
|
43
41
|
clientsStatus: null,
|
|
44
|
-
offlineAt: null,
|
|
45
42
|
isActiveInternalChat: true,
|
|
43
|
+
internalChatToken: null,
|
|
44
|
+
noteReference: false,
|
|
45
|
+
data: {},
|
|
46
46
|
passwordExpiresAt: null,
|
|
47
47
|
otpAuthActive: false,
|
|
48
48
|
preferences: {},
|
|
@@ -45,6 +45,17 @@ const message: Message = {
|
|
|
45
45
|
obfuscated: false,
|
|
46
46
|
reactionParentMessageId: null,
|
|
47
47
|
hsmId: null,
|
|
48
|
+
isFromBot: false,
|
|
49
|
+
accountId: 'acc-1',
|
|
50
|
+
serviceId: 'svc-1',
|
|
51
|
+
contactId: 'ctt-1',
|
|
52
|
+
fromId: null,
|
|
53
|
+
toId: 'ctt-1',
|
|
54
|
+
userId: 'usr-1',
|
|
55
|
+
ticketId: 'tkt-1',
|
|
56
|
+
ticketUserId: 'usr-1',
|
|
57
|
+
ticketDepartmentId: 'dep-1',
|
|
58
|
+
quotedMessageId: null,
|
|
48
59
|
createdAt: '2024-01-01T00:00:00Z',
|
|
49
60
|
updatedAt: '2024-01-01T00:00:00Z',
|
|
50
61
|
deletedAt: null,
|
|
@@ -85,7 +96,7 @@ describe('MessagesApi', () => {
|
|
|
85
96
|
it('serializes query parameters', async () => {
|
|
86
97
|
vi.mocked(client.get).mockResolvedValue(paginated)
|
|
87
98
|
await api.getMany({ page: 2, perPage: 5 })
|
|
88
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
99
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
89
100
|
expect(url).toContain('/messages?')
|
|
90
101
|
expect(decodeURIComponent(url)).toContain('"page":2')
|
|
91
102
|
expect(decodeURIComponent(url)).toContain('"perPage":5')
|
|
@@ -382,7 +393,7 @@ describe('MessagesApi', () => {
|
|
|
382
393
|
it('appends limit query parameter when provided', async () => {
|
|
383
394
|
vi.mocked(client.get).mockResolvedValue({ outOfRange: true })
|
|
384
395
|
await api.outOfRange('msg-1', 50)
|
|
385
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
396
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
386
397
|
expect(url).toContain('/messages/msg-1/out-of-range?')
|
|
387
398
|
expect(url).toContain('limit=50')
|
|
388
399
|
})
|
|
@@ -426,7 +437,7 @@ describe('MessagesApi', () => {
|
|
|
426
437
|
it('appends limit query parameter when provided', async () => {
|
|
427
438
|
vi.mocked(client.get).mockResolvedValue([])
|
|
428
439
|
await api.around('msg-1', { limit: 10 })
|
|
429
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
440
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
430
441
|
expect(url).toContain('/messages/msg-1/around?')
|
|
431
442
|
expect(url).toContain('limit=10')
|
|
432
443
|
})
|
|
@@ -101,9 +101,12 @@ export class MessagesApi extends BaseCrudApi<Message, CreateMessagePayload, Upda
|
|
|
101
101
|
)
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Triggers an AI-generated summary for the conversation associated with the given message.
|
|
106
|
+
* Resolves to `'OK'`.
|
|
107
|
+
*/
|
|
108
|
+
summarize(id: string, headers?: Record<string, string>): Promise<string> {
|
|
109
|
+
return this.client.post<string>(`${this.basePath}/${id}/summarize`, undefined, headers)
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
/** Checks whether a message is out of the visible range (i.e. there are more than `limit` messages after it). */
|
|
@@ -113,6 +113,19 @@ export type Message = {
|
|
|
113
113
|
transcribeError?: boolean
|
|
114
114
|
reactionParentMessageId: string | null
|
|
115
115
|
hsmId: string | null
|
|
116
|
+
isFromBot: boolean
|
|
117
|
+
accountId: string
|
|
118
|
+
serviceId: string
|
|
119
|
+
contactId: string
|
|
120
|
+
/** The contact that sent the message; `null` when it is from the account. */
|
|
121
|
+
fromId: string | null
|
|
122
|
+
toId: string | null
|
|
123
|
+
/** The user that sent the message; `null` for a received one. */
|
|
124
|
+
userId: string | null
|
|
125
|
+
ticketId: string | null
|
|
126
|
+
ticketUserId: string | null
|
|
127
|
+
ticketDepartmentId: string | null
|
|
128
|
+
quotedMessageId: string | null
|
|
116
129
|
createdAt: string
|
|
117
130
|
updatedAt: string
|
|
118
131
|
deletedAt: string | null
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { expectError } from 'tsd'
|
|
2
2
|
import type { ApiClient } from '../core/ApiClient'
|
|
3
|
+
import { ContactsApi } from './contacts/ContactsApi'
|
|
3
4
|
import { PersonalAccessTokensApi } from './personalAccessTokens/PersonalAccessTokensApi'
|
|
4
5
|
import { RolesApi } from './roles/RolesApi'
|
|
6
|
+
import { TagsApi } from './tags/TagsApi'
|
|
5
7
|
import { TimetableApi } from './timetable/TimetableApi'
|
|
8
|
+
import { UsersApi } from './users/UsersApi'
|
|
6
9
|
|
|
7
10
|
declare const client: ApiClient
|
|
8
11
|
|
|
@@ -37,3 +40,26 @@ tokens.create({ name: 'CI', expiresIn: '90d' })
|
|
|
37
40
|
expectError(tokens.create({ name: 'CI', accessTokenExpiresAt: '2030-01-01T00:00:00Z' }))
|
|
38
41
|
expectError(tokens.create({ name: 'CI', expiresIn: '7d' }))
|
|
39
42
|
expectError(tokens.updateById('t-1', { scope: '*' }))
|
|
43
|
+
|
|
44
|
+
// ── users: the dedicated archive route takes a boolean, not a date ──
|
|
45
|
+
|
|
46
|
+
const users = new UsersApi(client)
|
|
47
|
+
users.archiveWithValidation('u-1', { archive: true })
|
|
48
|
+
expectError(users.archiveWithValidation('u-1', { archivedAt: '2030-01-01T00:00:00Z' }))
|
|
49
|
+
expectError(users.exportCsv({ filter: 'deleted' }))
|
|
50
|
+
|
|
51
|
+
// ── contacts: /many takes an array, the bulk delete a selection ─────
|
|
52
|
+
|
|
53
|
+
const contacts = new ContactsApi(client)
|
|
54
|
+
contacts.createMany([{ serviceId: 's-1', name: 'Alice', number: '5511999990001' }])
|
|
55
|
+
expectError(contacts.createMany({ serviceId: 's-1', name: 'Alice', number: '5511999990001' }))
|
|
56
|
+
expectError(contacts.getMedia('c-1', { where: { type: 'audio' } }))
|
|
57
|
+
|
|
58
|
+
// ── contact selections: an empty filter would reach every contact of the account ─
|
|
59
|
+
|
|
60
|
+
contacts.deleteMany({ where: { id: 'c-1' } })
|
|
61
|
+
expectError(contacts.deleteMany({}))
|
|
62
|
+
const tags = new TagsApi(client)
|
|
63
|
+
tags.attachContacts('t-1', { where: { id: { $in: ['c-1', 'c-2'] } } })
|
|
64
|
+
expectError(tags.attachContacts('t-1', {}))
|
|
65
|
+
expectError(tags.detachContacts('t-1', { include: ['tags'] }))
|
|
@@ -32,6 +32,8 @@ function createMockClient(queryFormat?: QueryFormat): ApiClient {
|
|
|
32
32
|
|
|
33
33
|
const listQuery = { where: { name: 'x' }, page: 2, perPage: 5 }
|
|
34
34
|
const getQuery = { attributes: ['id'] }
|
|
35
|
+
/** `Permission.name` is a union of the catalog names, so the permissions case filters by a real one. */
|
|
36
|
+
const permissionsQuery = { where: { name: 'chat' as const }, page: 2, perPage: 5 }
|
|
35
37
|
|
|
36
38
|
type Case = {
|
|
37
39
|
name: string
|
|
@@ -69,6 +71,27 @@ const cases: Case[] = [
|
|
|
69
71
|
return (f ? api.withQueryFormat(f) : api).count(listQuery)
|
|
70
72
|
},
|
|
71
73
|
},
|
|
74
|
+
{
|
|
75
|
+
name: 'ContactsApi.getMedia',
|
|
76
|
+
path: '/contacts/c1/media',
|
|
77
|
+
query: { where: { type: 'document' }, perPage: 10 },
|
|
78
|
+
call: (client, f) => {
|
|
79
|
+
const api = new ContactsApi(client)
|
|
80
|
+
return (f ? api.withQueryFormat(f) : api).getMedia('c1', {
|
|
81
|
+
where: { type: 'document' },
|
|
82
|
+
perPage: 10,
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'ContactsApi.createMany',
|
|
88
|
+
path: '/contacts/many',
|
|
89
|
+
query: { include: ['tags'] },
|
|
90
|
+
call: (client, f) => {
|
|
91
|
+
const api = new ContactsApi(client)
|
|
92
|
+
return (f ? api.withQueryFormat(f) : api).createMany([], { include: ['tags'] })
|
|
93
|
+
},
|
|
94
|
+
},
|
|
72
95
|
{
|
|
73
96
|
name: 'ContactsApi.forward',
|
|
74
97
|
path: '/contacts/forward',
|
|
@@ -199,10 +222,10 @@ const cases: Case[] = [
|
|
|
199
222
|
{
|
|
200
223
|
name: 'PermissionsApi.getMany',
|
|
201
224
|
path: '/permissions',
|
|
202
|
-
query:
|
|
225
|
+
query: permissionsQuery,
|
|
203
226
|
call: (client, f) => {
|
|
204
227
|
const api = new PermissionsApi(client)
|
|
205
|
-
return (f ? api.withQueryFormat(f) : api).getMany(
|
|
228
|
+
return (f ? api.withQueryFormat(f) : api).getMany(permissionsQuery)
|
|
206
229
|
},
|
|
207
230
|
},
|
|
208
231
|
{
|
|
@@ -255,7 +278,8 @@ const cases: Case[] = [
|
|
|
255
278
|
const jsonUrl = (path: string, query: object) =>
|
|
256
279
|
`${path}?query=${encodeURIComponent(JSON.stringify(query))}`
|
|
257
280
|
const qsUrl = (path: string, query: object) => `${path}?${qs.stringify(query)}`
|
|
258
|
-
const calledUrl = (client: ApiClient) =>
|
|
281
|
+
const calledUrl = (client: ApiClient) =>
|
|
282
|
+
vi.mocked(client.get).mock.calls.at(0)?.at(0) ?? vi.mocked(client.post).mock.calls.at(0)?.at(0)
|
|
259
283
|
|
|
260
284
|
describe.each(cases)('$name', ({ path, query, call }) => {
|
|
261
285
|
it("sends JSON with the default client format ('json')", async () => {
|
|
@@ -61,7 +61,7 @@ describe('ServiceAccessManagementApi', () => {
|
|
|
61
61
|
it('serializes query parameters', async () => {
|
|
62
62
|
vi.mocked(client.get).mockResolvedValue(paginated)
|
|
63
63
|
await api.getMany({ page: 2, perPage: 5 })
|
|
64
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
64
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
65
65
|
expect(url).toContain('/service-access-management?')
|
|
66
66
|
expect(decodeURIComponent(url)).toContain('"page":2')
|
|
67
67
|
})
|
|
@@ -76,7 +76,7 @@ describe('ServiceAccessManagementApi', () => {
|
|
|
76
76
|
it('returns unpaginated array when paginate: false', async () => {
|
|
77
77
|
vi.mocked(client.get).mockResolvedValue([record])
|
|
78
78
|
const result = await api.getMany({ paginate: false })
|
|
79
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
79
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
80
80
|
expect(decodeURIComponent(url)).toContain('"paginate":false')
|
|
81
81
|
expect(result).toEqual([record])
|
|
82
82
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { ServicesApi } from './ServicesApi'
|
|
3
|
+
import type { ApiClient } from '../../core/ApiClient'
|
|
4
|
+
import type { ServiceLimits } from './types'
|
|
5
|
+
|
|
6
|
+
// ── Mock client factory ──────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function createMockClient(): ApiClient {
|
|
9
|
+
return {
|
|
10
|
+
setAccessToken: vi.fn().mockReturnThis(),
|
|
11
|
+
request: vi.fn(),
|
|
12
|
+
get: vi.fn(),
|
|
13
|
+
post: vi.fn(),
|
|
14
|
+
put: vi.fn(),
|
|
15
|
+
patch: vi.fn(),
|
|
16
|
+
delete: vi.fn(),
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
describe('ServicesApi', () => {
|
|
23
|
+
let client: ReturnType<typeof createMockClient>
|
|
24
|
+
let api: ServicesApi
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
client = createMockClient()
|
|
28
|
+
api = new ServicesApi(client)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('getLimits', () => {
|
|
32
|
+
it('calls client.get with /services/limits', async () => {
|
|
33
|
+
const limits: ServiceLimits = {
|
|
34
|
+
whatsapp: { used: 1, total: 80, available: 79 },
|
|
35
|
+
telegram: { used: 0, total: 0, available: 0 },
|
|
36
|
+
}
|
|
37
|
+
vi.mocked(client.get).mockResolvedValue(limits)
|
|
38
|
+
const result = await api.getLimits()
|
|
39
|
+
expect(client.get).toHaveBeenCalledWith('/services/limits', undefined)
|
|
40
|
+
expect(result).toEqual(limits)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('forwards custom headers', async () => {
|
|
44
|
+
const headers = { 'X-Custom': 'value' }
|
|
45
|
+
await api.getLimits(headers)
|
|
46
|
+
expect(client.get).toHaveBeenCalledWith('/services/limits', headers)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ApiClient } from '../../core/ApiClient'
|
|
2
2
|
import { ArchivableCrudApi } from '../../core/ArchivableCrudApi'
|
|
3
|
-
import type { Service, CreateServicePayload, UpdateServicePayload } from './types'
|
|
3
|
+
import type { Service, CreateServicePayload, ServiceLimits, UpdateServicePayload } from './types'
|
|
4
4
|
|
|
5
5
|
export class ServicesApi extends ArchivableCrudApi<
|
|
6
6
|
Service,
|
|
@@ -10,4 +10,13 @@ export class ServicesApi extends ArchivableCrudApi<
|
|
|
10
10
|
constructor(client: ApiClient) {
|
|
11
11
|
super(client, '/services')
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns, for each connection type of the account plan, how many connections are used,
|
|
16
|
+
* allowed and still available.
|
|
17
|
+
* @permissions services.view
|
|
18
|
+
*/
|
|
19
|
+
getLimits(headers?: Record<string, string>): Promise<ServiceLimits> {
|
|
20
|
+
return this.client.get<ServiceLimits>(`${this.basePath}/limits`, headers)
|
|
21
|
+
}
|
|
13
22
|
}
|
|
@@ -221,3 +221,12 @@ export type UpdateServicePayload = {
|
|
|
221
221
|
botId?: string | null
|
|
222
222
|
archivedAt?: string | null
|
|
223
223
|
}
|
|
224
|
+
|
|
225
|
+
export type ServiceLimit = {
|
|
226
|
+
used: number
|
|
227
|
+
total: number
|
|
228
|
+
available: number
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Connections used and allowed by the account plan, by connection type. */
|
|
232
|
+
export type ServiceLimits = Partial<Record<ServiceType, ServiceLimit>>
|
|
@@ -64,7 +64,7 @@ describe('StickerUsersApi', () => {
|
|
|
64
64
|
it('serializes query parameters', async () => {
|
|
65
65
|
vi.mocked(client.get).mockResolvedValue(paginated)
|
|
66
66
|
await api.getMany({ page: 2, perPage: 5 })
|
|
67
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
67
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
68
68
|
expect(url).toContain('/sticker-users?')
|
|
69
69
|
expect(decodeURIComponent(url)).toContain('"page":2')
|
|
70
70
|
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { TagsApi } from './TagsApi'
|
|
3
|
+
import type { ApiClient } from '../../core/ApiClient'
|
|
4
|
+
|
|
5
|
+
// ── Mock client factory ──────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
function createMockClient(): ApiClient {
|
|
8
|
+
return {
|
|
9
|
+
setAccessToken: vi.fn().mockReturnThis(),
|
|
10
|
+
request: vi.fn(),
|
|
11
|
+
get: vi.fn(),
|
|
12
|
+
post: vi.fn(),
|
|
13
|
+
put: vi.fn(),
|
|
14
|
+
patch: vi.fn(),
|
|
15
|
+
delete: vi.fn(),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const headers = { 'X-Custom': 'value' }
|
|
20
|
+
|
|
21
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
describe('TagsApi', () => {
|
|
24
|
+
let client: ReturnType<typeof createMockClient>
|
|
25
|
+
let api: TagsApi
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
client = createMockClient()
|
|
29
|
+
api = new TagsApi(client)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('attachContacts', () => {
|
|
33
|
+
it('calls client.post with /tags/:id/contacts and the selection', async () => {
|
|
34
|
+
vi.mocked(client.post).mockResolvedValue('OK')
|
|
35
|
+
const body = { where: { id: { $in: ['c1', 'c2'] } } }
|
|
36
|
+
const result = await api.attachContacts('t1', body)
|
|
37
|
+
expect(client.post).toHaveBeenCalledWith('/tags/t1/contacts', body, undefined)
|
|
38
|
+
expect(result).toBe('OK')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('resolves to false when no link changed', async () => {
|
|
42
|
+
vi.mocked(client.post).mockResolvedValue(false)
|
|
43
|
+
await expect(api.attachContacts('t1', { where: { id: 'c1' } })).resolves.toBe(false)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('forwards custom headers', async () => {
|
|
47
|
+
await api.attachContacts('t1', { where: { id: 'c1' } }, headers)
|
|
48
|
+
expect(client.post).toHaveBeenCalledWith(
|
|
49
|
+
'/tags/t1/contacts',
|
|
50
|
+
{ where: { id: 'c1' } },
|
|
51
|
+
headers,
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('detachContacts', () => {
|
|
57
|
+
it('calls client.request with DELETE /tags/:id/contacts and the selection', async () => {
|
|
58
|
+
vi.mocked(client.request).mockResolvedValue('OK')
|
|
59
|
+
const body = { where: { id: 'c1' } }
|
|
60
|
+
const result = await api.detachContacts('t1', body, headers)
|
|
61
|
+
expect(client.request).toHaveBeenCalledWith('DELETE', '/tags/t1/contacts', body, headers)
|
|
62
|
+
expect(result).toBe('OK')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('attachManyToDepartments', () => {
|
|
67
|
+
it('calls client.post with /tags/attach-many-departments and body', async () => {
|
|
68
|
+
vi.mocked(client.post).mockResolvedValue('OK')
|
|
69
|
+
const body = { ids: ['t1', 't2'], departmentIds: ['d1'] }
|
|
70
|
+
const result = await api.attachManyToDepartments(body)
|
|
71
|
+
expect(client.post).toHaveBeenCalledWith('/tags/attach-many-departments', body, undefined)
|
|
72
|
+
expect(result).toBe('OK')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('error propagation', () => {
|
|
77
|
+
it('propagates errors from attachManyToDepartments', async () => {
|
|
78
|
+
vi.mocked(client.post).mockRejectedValue(new Error('Invalid tag ids'))
|
|
79
|
+
await expect(api.attachManyToDepartments({ ids: [], departmentIds: [] })).rejects.toThrow(
|
|
80
|
+
'Invalid tag ids',
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
})
|
package/src/apis/tags/TagsApi.ts
CHANGED
|
@@ -1,9 +1,67 @@
|
|
|
1
1
|
import type { ApiClient } from '../../core/ApiClient'
|
|
2
2
|
import { BaseCrudApi } from '../../core/BaseCrudApi'
|
|
3
|
-
import type {
|
|
3
|
+
import type { ContactsSelection } from '../contacts/types'
|
|
4
|
+
import type {
|
|
5
|
+
AttachTagsToDepartmentsPayload,
|
|
6
|
+
CreateTagPayload,
|
|
7
|
+
Tag,
|
|
8
|
+
TagContactsLinkResult,
|
|
9
|
+
UpdateTagPayload,
|
|
10
|
+
} from './types'
|
|
4
11
|
|
|
5
12
|
export class TagsApi extends BaseCrudApi<Tag, CreateTagPayload, UpdateTagPayload> {
|
|
6
13
|
constructor(client: ApiClient) {
|
|
7
14
|
super(client, '/tags')
|
|
8
15
|
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Adds the tag to every contact of the account that matches the selection. Resolves to `'OK'`,
|
|
19
|
+
* or `false` when every contact already had the tag.
|
|
20
|
+
* @permissions tags.view
|
|
21
|
+
*/
|
|
22
|
+
attachContacts(
|
|
23
|
+
id: string,
|
|
24
|
+
body: ContactsSelection,
|
|
25
|
+
headers?: Record<string, string>,
|
|
26
|
+
): Promise<TagContactsLinkResult> {
|
|
27
|
+
return this.client.post<TagContactsLinkResult, ContactsSelection>(
|
|
28
|
+
`${this.basePath}/${id}/contacts`,
|
|
29
|
+
body,
|
|
30
|
+
headers,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Removes the tag from every contact of the account that matches the selection. Resolves to
|
|
36
|
+
* `'OK'`, or `false` when no contact had the tag.
|
|
37
|
+
* @permissions tags.view OR tags.destroy
|
|
38
|
+
*/
|
|
39
|
+
detachContacts(
|
|
40
|
+
id: string,
|
|
41
|
+
body: ContactsSelection,
|
|
42
|
+
headers?: Record<string, string>,
|
|
43
|
+
): Promise<TagContactsLinkResult> {
|
|
44
|
+
return this.client.request<TagContactsLinkResult, ContactsSelection>(
|
|
45
|
+
'DELETE',
|
|
46
|
+
`${this.basePath}/${id}/contacts`,
|
|
47
|
+
body,
|
|
48
|
+
headers,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Links several tags to the given departments. Resolves to `'OK'`. A tag without departments
|
|
54
|
+
* is already available to all of them and is left unchanged.
|
|
55
|
+
* @permissions tags.view
|
|
56
|
+
*/
|
|
57
|
+
attachManyToDepartments(
|
|
58
|
+
body: AttachTagsToDepartmentsPayload,
|
|
59
|
+
headers?: Record<string, string>,
|
|
60
|
+
): Promise<string> {
|
|
61
|
+
return this.client.post<string, AttachTagsToDepartmentsPayload>(
|
|
62
|
+
`${this.basePath}/attach-many-departments`,
|
|
63
|
+
body,
|
|
64
|
+
headers,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
9
67
|
}
|
package/src/apis/tags/types.ts
CHANGED
|
@@ -27,3 +27,18 @@ export type UpdateTagPayload = {
|
|
|
27
27
|
label?: string
|
|
28
28
|
backgroundColor?: string | null
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Result of linking or unlinking a tag and the contacts of a selection: `'OK'` when at least one
|
|
33
|
+
* link changed, `false` when nothing changed (every contact already had, or did not have, the tag).
|
|
34
|
+
*/
|
|
35
|
+
export type TagContactsLinkResult = 'OK' | false
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Body for `POST /tags/attach-many-departments`. An empty `departmentIds` makes the tags
|
|
39
|
+
* available to every department.
|
|
40
|
+
*/
|
|
41
|
+
export type AttachTagsToDepartmentsPayload = {
|
|
42
|
+
ids: string[]
|
|
43
|
+
departmentIds: string[]
|
|
44
|
+
}
|
|
@@ -66,7 +66,7 @@ describe('TermsApi', () => {
|
|
|
66
66
|
it('serializes the query', async () => {
|
|
67
67
|
vi.mocked(client.get).mockResolvedValue(term)
|
|
68
68
|
await api.getById('term-1', { attributes: ['id', 'version'] })
|
|
69
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
69
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
70
70
|
expect(url).toContain('/terms/term-1?')
|
|
71
71
|
expect(url).toContain('attributes')
|
|
72
72
|
})
|