@ikatec/digisac-api-sdk 4.0.0 → 4.1.0
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/README.md +29 -2
- package/dist/apis/authHistory/AuthHistoryApi.cjs +3 -9
- package/dist/apis/authHistory/AuthHistoryApi.cjs.map +1 -1
- package/dist/apis/authHistory/AuthHistoryApi.d.ts +3 -4
- package/dist/apis/authHistory/AuthHistoryApi.d.ts.map +1 -1
- package/dist/apis/authHistory/AuthHistoryApi.mjs +3 -7
- package/dist/apis/authHistory/AuthHistoryApi.mjs.map +1 -1
- package/dist/apis/clientFeedback/ClientFeedbackApi.cjs +2 -5
- package/dist/apis/clientFeedback/ClientFeedbackApi.cjs.map +1 -1
- package/dist/apis/clientFeedback/ClientFeedbackApi.d.ts +2 -4
- package/dist/apis/clientFeedback/ClientFeedbackApi.d.ts.map +1 -1
- package/dist/apis/clientFeedback/ClientFeedbackApi.mjs +3 -5
- package/dist/apis/clientFeedback/ClientFeedbackApi.mjs.map +1 -1
- package/dist/apis/contacts/ContactsApi.cjs +2 -2
- package/dist/apis/contacts/ContactsApi.cjs.map +1 -1
- package/dist/apis/contacts/ContactsApi.mjs +2 -2
- package/dist/apis/contacts/ContactsApi.mjs.map +1 -1
- package/dist/apis/me/MeApi.cjs +2 -5
- package/dist/apis/me/MeApi.cjs.map +1 -1
- package/dist/apis/me/MeApi.d.ts +2 -4
- package/dist/apis/me/MeApi.d.ts.map +1 -1
- package/dist/apis/me/MeApi.mjs +3 -5
- package/dist/apis/me/MeApi.mjs.map +1 -1
- package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.cjs +3 -9
- package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.cjs.map +1 -1
- package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.d.ts +3 -4
- package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.d.ts.map +1 -1
- package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.mjs +3 -7
- package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.mjs.map +1 -1
- package/dist/apis/stickerUsers/StickerUsersApi.cjs +3 -9
- package/dist/apis/stickerUsers/StickerUsersApi.cjs.map +1 -1
- package/dist/apis/stickerUsers/StickerUsersApi.d.ts +3 -4
- package/dist/apis/stickerUsers/StickerUsersApi.d.ts.map +1 -1
- package/dist/apis/stickerUsers/StickerUsersApi.mjs +3 -7
- package/dist/apis/stickerUsers/StickerUsersApi.mjs.map +1 -1
- package/dist/apis/terms/TermsApi.cjs +3 -12
- package/dist/apis/terms/TermsApi.cjs.map +1 -1
- package/dist/apis/terms/TermsApi.d.ts +2 -4
- package/dist/apis/terms/TermsApi.d.ts.map +1 -1
- package/dist/apis/terms/TermsApi.mjs +3 -10
- package/dist/apis/terms/TermsApi.mjs.map +1 -1
- package/dist/apis/transcripts/TranscriptsApi.cjs +2 -5
- package/dist/apis/transcripts/TranscriptsApi.cjs.map +1 -1
- package/dist/apis/transcripts/TranscriptsApi.d.ts +2 -4
- package/dist/apis/transcripts/TranscriptsApi.d.ts.map +1 -1
- package/dist/apis/transcripts/TranscriptsApi.mjs +3 -5
- package/dist/apis/transcripts/TranscriptsApi.mjs.map +1 -1
- package/dist/core/ApiClient.d.ts +3 -0
- package/dist/core/ApiClient.d.ts.map +1 -1
- package/dist/core/ArchivableCrudApi.cjs +0 -2
- package/dist/core/ArchivableCrudApi.cjs.map +1 -1
- package/dist/core/ArchivableCrudApi.d.ts.map +1 -1
- package/dist/core/ArchivableCrudApi.mjs +0 -2
- package/dist/core/ArchivableCrudApi.mjs.map +1 -1
- package/dist/core/BaseApi.cjs +34 -0
- package/dist/core/BaseApi.cjs.map +1 -0
- package/dist/core/BaseApi.d.ts +20 -0
- package/dist/core/BaseApi.d.ts.map +1 -0
- package/dist/core/BaseApi.mjs +33 -0
- package/dist/core/BaseApi.mjs.map +1 -0
- package/dist/core/BaseApiClient.cjs +4 -1
- package/dist/core/BaseApiClient.cjs.map +1 -1
- package/dist/core/BaseApiClient.d.ts +7 -1
- package/dist/core/BaseApiClient.d.ts.map +1 -1
- package/dist/core/BaseApiClient.mjs +4 -1
- package/dist/core/BaseApiClient.mjs.map +1 -1
- package/dist/core/BaseCrudApi.cjs +6 -16
- package/dist/core/BaseCrudApi.cjs.map +1 -1
- package/dist/core/BaseCrudApi.d.ts +3 -2
- package/dist/core/BaseCrudApi.d.ts.map +1 -1
- package/dist/core/BaseCrudApi.mjs +6 -14
- package/dist/core/BaseCrudApi.mjs.map +1 -1
- package/dist/core/bindMethods.cjs +24 -0
- package/dist/core/bindMethods.cjs.map +1 -0
- package/dist/core/bindMethods.d.ts +7 -0
- package/dist/core/bindMethods.d.ts.map +1 -0
- package/dist/core/bindMethods.mjs +22 -0
- package/dist/core/bindMethods.mjs.map +1 -0
- package/dist/core/index.cjs +2 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.mjs +2 -1
- package/dist/core/queryString.cjs +22 -0
- package/dist/core/queryString.cjs.map +1 -0
- package/dist/core/queryString.d.ts +14 -0
- package/dist/core/queryString.d.ts.map +1 -0
- package/dist/core/queryString.mjs +18 -0
- package/dist/core/queryString.mjs.map +1 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/withQueryFormat.cjs +42 -0
- package/dist/core/withQueryFormat.cjs.map +1 -0
- package/dist/core/withQueryFormat.d.ts +10 -0
- package/dist/core/withQueryFormat.d.ts.map +1 -0
- package/dist/core/withQueryFormat.mjs +40 -0
- package/dist/core/withQueryFormat.mjs.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.mjs +2 -1
- package/package.json +1 -1
- package/src/apis/authHistory/AuthHistoryApi.test.ts +1 -1
- package/src/apis/authHistory/AuthHistoryApi.ts +10 -10
- package/src/apis/campaigns/CampaignsApi.test.ts +1 -1
- package/src/apis/clientFeedback/ClientFeedbackApi.ts +5 -7
- package/src/apis/contacts/ContactsApi.test.ts +1 -1
- package/src/apis/contacts/ContactsApi.ts +2 -2
- package/src/apis/me/MeApi.ts +2 -8
- package/src/apis/messages/MessagesApi.test.ts +2 -2
- package/src/apis/queryFormat.test.ts +198 -0
- package/src/apis/serviceAccessManagement/ServiceAccessManagementApi.test.ts +2 -2
- package/src/apis/serviceAccessManagement/ServiceAccessManagementApi.ts +10 -10
- package/src/apis/stickerUsers/StickerUsersApi.test.ts +1 -1
- package/src/apis/stickerUsers/StickerUsersApi.ts +7 -10
- package/src/apis/terms/TermsApi.ts +3 -12
- package/src/apis/transcripts/TranscriptsApi.ts +2 -7
- package/src/core/ApiClient.ts +4 -0
- package/src/core/ArchivableCrudApi.ts +0 -2
- package/src/core/BaseApi.test.ts +164 -0
- package/src/core/BaseApi.ts +32 -0
- package/src/core/BaseApiClient.test.ts +10 -0
- package/src/core/BaseApiClient.ts +9 -1
- package/src/core/BaseCrudApi.test-d.ts +9 -0
- package/src/core/BaseCrudApi.test.ts +131 -60
- package/src/core/BaseCrudApi.ts +14 -14
- package/src/core/bindMethods.ts +26 -0
- package/src/core/index.ts +3 -0
- package/src/core/queryString.test.ts +71 -0
- package/src/core/queryString.ts +30 -0
- package/src/core/types.ts +5 -0
- package/src/core/withQueryFormat.test.ts +136 -0
- package/src/core/withQueryFormat.ts +47 -0
|
@@ -35,7 +35,7 @@ export class ContactsApi extends BaseCrudApi<Contact, CreateContactPayload, Upda
|
|
|
35
35
|
* @permissions contacts.view
|
|
36
36
|
*/
|
|
37
37
|
count(query?: CountContactsQuery, headers?: Record<string, string>): Promise<{ count: number }> {
|
|
38
|
-
const queryString =
|
|
38
|
+
const queryString = this.toQueryString(query)
|
|
39
39
|
return this.client.get<{ count: number }>(`${this.basePath}/count${queryString}`, headers)
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -62,7 +62,7 @@ export class ContactsApi extends BaseCrudApi<Contact, CreateContactPayload, Upda
|
|
|
62
62
|
query?: ListQuery<Contact>,
|
|
63
63
|
headers?: Record<string, string>,
|
|
64
64
|
): Promise<Paginated<Contact>> {
|
|
65
|
-
const queryString =
|
|
65
|
+
const queryString = this.toQueryString(query)
|
|
66
66
|
return this.client.get<Paginated<Contact>>(`${this.basePath}/forward${queryString}`, headers)
|
|
67
67
|
}
|
|
68
68
|
|
package/src/apis/me/MeApi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { BaseApi } from '../../core/BaseApi'
|
|
2
2
|
import type { Account } from '../accounts/types'
|
|
3
3
|
import type { User } from '../users/types'
|
|
4
4
|
import type {
|
|
@@ -10,13 +10,7 @@ import type {
|
|
|
10
10
|
UpdateMeStatusPayload,
|
|
11
11
|
} from './types'
|
|
12
12
|
|
|
13
|
-
export class MeApi {
|
|
14
|
-
protected readonly client: ApiClient
|
|
15
|
-
|
|
16
|
-
constructor(client: ApiClient) {
|
|
17
|
-
this.client = client
|
|
18
|
-
}
|
|
19
|
-
|
|
13
|
+
export class MeApi extends BaseApi {
|
|
20
14
|
/**
|
|
21
15
|
* Returns the currently authenticated user, including their roles, departments, and timetable.
|
|
22
16
|
* @permissions users.view
|
|
@@ -87,8 +87,8 @@ describe('MessagesApi', () => {
|
|
|
87
87
|
await api.getMany({ page: 2, perPage: 5 })
|
|
88
88
|
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
89
89
|
expect(url).toContain('/messages?')
|
|
90
|
-
expect(url).toContain('page
|
|
91
|
-
expect(url).toContain('perPage
|
|
90
|
+
expect(decodeURIComponent(url)).toContain('"page":2')
|
|
91
|
+
expect(decodeURIComponent(url)).toContain('"perPage":5')
|
|
92
92
|
})
|
|
93
93
|
|
|
94
94
|
it('forwards custom headers', async () => {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import qs from 'qs'
|
|
3
|
+
import type { ApiClient } from '../core/ApiClient'
|
|
4
|
+
import { BaseApiClient } from '../core/BaseApiClient'
|
|
5
|
+
import type { QueryFormat } from '../core/queryString'
|
|
6
|
+
import { AuthHistoryApi } from './authHistory/AuthHistoryApi'
|
|
7
|
+
import { ContactsApi } from './contacts/ContactsApi'
|
|
8
|
+
import { ServiceAccessManagementApi } from './serviceAccessManagement/ServiceAccessManagementApi'
|
|
9
|
+
import { StickerUsersApi } from './stickerUsers/StickerUsersApi'
|
|
10
|
+
import { TermsApi } from './terms/TermsApi'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Every method that sends a list/get query must serialize it in the client's `queryFormat` and
|
|
14
|
+
* honor `withQueryFormat`. A method listed here that ignored the format would fail below.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function createMockClient(queryFormat?: QueryFormat): ApiClient {
|
|
18
|
+
return {
|
|
19
|
+
...(queryFormat && { queryFormat }),
|
|
20
|
+
setAccessToken: vi.fn().mockReturnThis(),
|
|
21
|
+
request: vi.fn(),
|
|
22
|
+
get: vi.fn().mockResolvedValue([]),
|
|
23
|
+
post: vi.fn(),
|
|
24
|
+
put: vi.fn(),
|
|
25
|
+
patch: vi.fn(),
|
|
26
|
+
delete: vi.fn(),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const listQuery = { where: { name: 'x' }, page: 2, perPage: 5 }
|
|
31
|
+
const getQuery = { attributes: ['id'] }
|
|
32
|
+
|
|
33
|
+
type Case = {
|
|
34
|
+
name: string
|
|
35
|
+
path: string
|
|
36
|
+
query: object
|
|
37
|
+
/** Calls the method on the API built from `client`, optionally through `withQueryFormat`. */
|
|
38
|
+
call: (client: ApiClient, override?: QueryFormat) => Promise<unknown>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cases: Case[] = [
|
|
42
|
+
{
|
|
43
|
+
name: 'ContactsApi.getMany (BaseCrudApi)',
|
|
44
|
+
path: '/contacts',
|
|
45
|
+
query: listQuery,
|
|
46
|
+
call: (client, f) => {
|
|
47
|
+
const api = new ContactsApi(client)
|
|
48
|
+
return (f ? api.withQueryFormat(f) : api).getMany(listQuery)
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'ContactsApi.getById (BaseCrudApi)',
|
|
53
|
+
path: '/contacts/c1',
|
|
54
|
+
query: getQuery,
|
|
55
|
+
call: (client, f) => {
|
|
56
|
+
const api = new ContactsApi(client)
|
|
57
|
+
return (f ? api.withQueryFormat(f) : api).getById('c1', { attributes: ['id'] })
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'ContactsApi.count',
|
|
62
|
+
path: '/contacts/count',
|
|
63
|
+
query: listQuery,
|
|
64
|
+
call: (client, f) => {
|
|
65
|
+
const api = new ContactsApi(client)
|
|
66
|
+
return (f ? api.withQueryFormat(f) : api).count(listQuery)
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'ContactsApi.forward',
|
|
71
|
+
path: '/contacts/forward',
|
|
72
|
+
query: listQuery,
|
|
73
|
+
call: (client, f) => {
|
|
74
|
+
const api = new ContactsApi(client)
|
|
75
|
+
return (f ? api.withQueryFormat(f) : api).forward(listQuery)
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'AuthHistoryApi.getMany',
|
|
80
|
+
path: '/auth-history',
|
|
81
|
+
query: { page: 2, perPage: 5 },
|
|
82
|
+
call: (client, f) => {
|
|
83
|
+
const api = new AuthHistoryApi(client)
|
|
84
|
+
return (f ? api.withQueryFormat(f) : api).getMany({ page: 2, perPage: 5 })
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'ServiceAccessManagementApi.getMany',
|
|
89
|
+
path: '/service-access-management',
|
|
90
|
+
query: { page: 2, perPage: 5 },
|
|
91
|
+
call: (client, f) => {
|
|
92
|
+
const api = new ServiceAccessManagementApi(client)
|
|
93
|
+
return (f ? api.withQueryFormat(f) : api).getMany({ page: 2, perPage: 5 })
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'StickerUsersApi.getMany',
|
|
98
|
+
path: '/sticker-users',
|
|
99
|
+
query: { page: 2, perPage: 5 },
|
|
100
|
+
call: (client, f) => {
|
|
101
|
+
const api = new StickerUsersApi(client)
|
|
102
|
+
return (f ? api.withQueryFormat(f) : api).getMany({ page: 2, perPage: 5 })
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'TermsApi.getById',
|
|
107
|
+
path: '/terms/t1',
|
|
108
|
+
query: getQuery,
|
|
109
|
+
call: (client, f) => {
|
|
110
|
+
const api = new TermsApi(client)
|
|
111
|
+
return (f ? api.withQueryFormat(f) : api).getById('t1', { attributes: ['id'] })
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
const jsonUrl = (path: string, query: object) =>
|
|
117
|
+
`${path}?query=${encodeURIComponent(JSON.stringify(query))}`
|
|
118
|
+
const qsUrl = (path: string, query: object) => `${path}?${qs.stringify(query)}`
|
|
119
|
+
const calledUrl = (client: ApiClient) => vi.mocked(client.get).mock.calls.at(0)?.at(0)
|
|
120
|
+
|
|
121
|
+
describe.each(cases)('$name', ({ path, query, call }) => {
|
|
122
|
+
it("sends JSON with the default client format ('json')", async () => {
|
|
123
|
+
const client = createMockClient()
|
|
124
|
+
await call(client)
|
|
125
|
+
expect(calledUrl(client)).toBe(jsonUrl(path, query))
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it("sends bracket notation with a 'qs' client", async () => {
|
|
129
|
+
const client = createMockClient('qs')
|
|
130
|
+
await call(client)
|
|
131
|
+
expect(calledUrl(client)).toBe(qsUrl(path, query))
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("withQueryFormat('qs') overrides a 'json' client", async () => {
|
|
135
|
+
const client = createMockClient()
|
|
136
|
+
await call(client, 'qs')
|
|
137
|
+
expect(calledUrl(client)).toBe(qsUrl(path, query))
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it("withQueryFormat('json') overrides a 'qs' client", async () => {
|
|
141
|
+
const client = createMockClient('qs')
|
|
142
|
+
await call(client, 'json')
|
|
143
|
+
expect(calledUrl(client)).toBe(jsonUrl(path, query))
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('query format through BaseApiClient', () => {
|
|
148
|
+
afterEach(() => {
|
|
149
|
+
vi.restoreAllMocks()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const mockFetch = () =>
|
|
153
|
+
vi
|
|
154
|
+
.spyOn(globalThis, 'fetch')
|
|
155
|
+
.mockImplementation(
|
|
156
|
+
async () =>
|
|
157
|
+
new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } }),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
it('builds the full ?query=<JSON> URL by default, keeping null typed', async () => {
|
|
161
|
+
const fetchSpy = mockFetch()
|
|
162
|
+
const contacts = new ContactsApi(new BaseApiClient('https://api.example.com/'))
|
|
163
|
+
|
|
164
|
+
await contacts.getMany({ where: { name: null }, paginate: false })
|
|
165
|
+
|
|
166
|
+
const query = '{"where":{"name":null},"paginate":false}'
|
|
167
|
+
expect(fetchSpy.mock.calls[0]?.[0]).toBe(
|
|
168
|
+
`https://api.example.com/contacts?query=${encodeURIComponent(query)}`,
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it("builds the bracket URL with queryFormat 'qs'", async () => {
|
|
173
|
+
const fetchSpy = mockFetch()
|
|
174
|
+
const client = new BaseApiClient('https://api.example.com', undefined, { queryFormat: 'qs' })
|
|
175
|
+
|
|
176
|
+
await new ContactsApi(client).getMany({ where: { name: 'x' }, paginate: false })
|
|
177
|
+
|
|
178
|
+
expect(decodeURIComponent(fetchSpy.mock.calls[0]?.[0] as string)).toBe(
|
|
179
|
+
'https://api.example.com/contacts?where[name]=x&paginate=false',
|
|
180
|
+
)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it("setAccessToken on a copy's client only affects the copy", async () => {
|
|
184
|
+
const fetchSpy = mockFetch()
|
|
185
|
+
const client = new BaseApiClient('https://api.example.com', 'original')
|
|
186
|
+
const contacts = new ContactsApi(client)
|
|
187
|
+
const copy = contacts.withQueryFormat('qs')
|
|
188
|
+
|
|
189
|
+
copy.client.setAccessToken('copy-only')
|
|
190
|
+
await contacts.getMany({ paginate: false })
|
|
191
|
+
await copy.getMany({ paginate: false })
|
|
192
|
+
|
|
193
|
+
const authorization = (call: number) =>
|
|
194
|
+
(fetchSpy.mock.calls[call]?.[1]?.headers as Record<string, string>).Authorization
|
|
195
|
+
expect(authorization(0)).toBe('Bearer original')
|
|
196
|
+
expect(authorization(1)).toBe('Bearer copy-only')
|
|
197
|
+
})
|
|
198
|
+
})
|
|
@@ -63,7 +63,7 @@ describe('ServiceAccessManagementApi', () => {
|
|
|
63
63
|
await api.getMany({ page: 2, perPage: 5 })
|
|
64
64
|
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
65
65
|
expect(url).toContain('/service-access-management?')
|
|
66
|
-
expect(url).toContain('page
|
|
66
|
+
expect(decodeURIComponent(url)).toContain('"page":2')
|
|
67
67
|
})
|
|
68
68
|
|
|
69
69
|
it('forwards custom headers', async () => {
|
|
@@ -77,7 +77,7 @@ describe('ServiceAccessManagementApi', () => {
|
|
|
77
77
|
vi.mocked(client.get).mockResolvedValue([record])
|
|
78
78
|
const result = await api.getMany({ paginate: false })
|
|
79
79
|
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
80
|
-
expect(url).toContain('paginate
|
|
80
|
+
expect(decodeURIComponent(url)).toContain('"paginate":false')
|
|
81
81
|
expect(result).toEqual([record])
|
|
82
82
|
})
|
|
83
83
|
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { ApiClient } from '../../core/ApiClient'
|
|
1
|
+
import { BaseApi } from '../../core/BaseApi'
|
|
3
2
|
import type { ListQuery, Paginated, HasGetMany } from '../../core/types'
|
|
4
3
|
import type { ServiceAccessManagement } from './types'
|
|
5
4
|
|
|
6
|
-
export class ServiceAccessManagementApi
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
this.client = client
|
|
11
|
-
}
|
|
12
|
-
|
|
5
|
+
export class ServiceAccessManagementApi
|
|
6
|
+
extends BaseApi
|
|
7
|
+
implements HasGetMany<ServiceAccessManagement>
|
|
8
|
+
{
|
|
13
9
|
/** Returns a paginated list of which users have access to which services. */
|
|
14
10
|
getMany(
|
|
15
11
|
query: ListQuery<ServiceAccessManagement> & { paginate: false },
|
|
@@ -19,11 +15,15 @@ export class ServiceAccessManagementApi implements HasGetMany<ServiceAccessManag
|
|
|
19
15
|
query?: ListQuery<ServiceAccessManagement> & { paginate?: true },
|
|
20
16
|
headers?: Record<string, string>,
|
|
21
17
|
): Promise<Paginated<ServiceAccessManagement>>
|
|
18
|
+
getMany(
|
|
19
|
+
query?: ListQuery<ServiceAccessManagement>,
|
|
20
|
+
headers?: Record<string, string>,
|
|
21
|
+
): Promise<Paginated<ServiceAccessManagement> | ServiceAccessManagement[]>
|
|
22
22
|
getMany(
|
|
23
23
|
query?: ListQuery<ServiceAccessManagement>,
|
|
24
24
|
headers?: Record<string, string>,
|
|
25
25
|
): Promise<Paginated<ServiceAccessManagement> | ServiceAccessManagement[]> {
|
|
26
|
-
const queryString =
|
|
26
|
+
const queryString = this.toQueryString(query)
|
|
27
27
|
return this.client.get<Paginated<ServiceAccessManagement> | ServiceAccessManagement[]>(
|
|
28
28
|
`/service-access-management${queryString}`,
|
|
29
29
|
headers,
|
|
@@ -66,7 +66,7 @@ describe('StickerUsersApi', () => {
|
|
|
66
66
|
await api.getMany({ page: 2, perPage: 5 })
|
|
67
67
|
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
68
68
|
expect(url).toContain('/sticker-users?')
|
|
69
|
-
expect(url).toContain('page
|
|
69
|
+
expect(decodeURIComponent(url)).toContain('"page":2')
|
|
70
70
|
})
|
|
71
71
|
|
|
72
72
|
it('forwards custom headers', async () => {
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { ApiClient } from '../../core/ApiClient'
|
|
1
|
+
import { BaseApi } from '../../core/BaseApi'
|
|
3
2
|
import type { ListQuery, Paginated, HasGetMany } from '../../core/types'
|
|
4
3
|
import type { StickerUser } from './types'
|
|
5
4
|
|
|
6
|
-
export class StickerUsersApi implements HasGetMany<StickerUser> {
|
|
7
|
-
private readonly client: ApiClient
|
|
8
|
-
|
|
9
|
-
constructor(client: ApiClient) {
|
|
10
|
-
this.client = client
|
|
11
|
-
}
|
|
12
|
-
|
|
5
|
+
export class StickerUsersApi extends BaseApi implements HasGetMany<StickerUser> {
|
|
13
6
|
/**
|
|
14
7
|
* Returns a paginated list of sticker-user associations with their related file.
|
|
15
8
|
* @permissions stickers.send
|
|
@@ -22,11 +15,15 @@ export class StickerUsersApi implements HasGetMany<StickerUser> {
|
|
|
22
15
|
query?: ListQuery<StickerUser> & { paginate?: true },
|
|
23
16
|
headers?: Record<string, string>,
|
|
24
17
|
): Promise<Paginated<StickerUser>>
|
|
18
|
+
getMany(
|
|
19
|
+
query?: ListQuery<StickerUser>,
|
|
20
|
+
headers?: Record<string, string>,
|
|
21
|
+
): Promise<Paginated<StickerUser> | StickerUser[]>
|
|
25
22
|
getMany(
|
|
26
23
|
query?: ListQuery<StickerUser>,
|
|
27
24
|
headers?: Record<string, string>,
|
|
28
25
|
): Promise<Paginated<StickerUser> | StickerUser[]> {
|
|
29
|
-
const queryString =
|
|
26
|
+
const queryString = this.toQueryString(query)
|
|
30
27
|
return this.client.get<Paginated<StickerUser> | StickerUser[]>(
|
|
31
28
|
`/sticker-users${queryString}`,
|
|
32
29
|
headers,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { ApiClient } from '../../core/ApiClient'
|
|
1
|
+
import { BaseApi } from '../../core/BaseApi'
|
|
3
2
|
import type { GetByIdQuery, HasGetById } from '../../core/types'
|
|
4
3
|
import type { Term } from './types'
|
|
5
4
|
|
|
@@ -10,20 +9,12 @@ import type { Term } from './types'
|
|
|
10
9
|
* `GET /terms/:id/export-pdf` is not exposed: it answers a binary PDF, and `ApiClient` only
|
|
11
10
|
* handles JSON/text bodies.
|
|
12
11
|
*/
|
|
13
|
-
export class TermsApi implements HasGetById<Term> {
|
|
14
|
-
protected readonly client: ApiClient
|
|
12
|
+
export class TermsApi extends BaseApi implements HasGetById<Term> {
|
|
15
13
|
public readonly basePath = '/terms'
|
|
16
14
|
|
|
17
|
-
constructor(client: ApiClient) {
|
|
18
|
-
this.client = client
|
|
19
|
-
this.getById = this.getById.bind(this)
|
|
20
|
-
this.getNotAgreed = this.getNotAgreed.bind(this)
|
|
21
|
-
this.agree = this.agree.bind(this)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
15
|
/** Returns a single term by its ID, scoped to the current account (`null` when not found). */
|
|
25
16
|
getById(id: string, query?: GetByIdQuery<Term>, headers?: Record<string, string>): Promise<Term> {
|
|
26
|
-
const queryString =
|
|
17
|
+
const queryString = this.toQueryString(query)
|
|
27
18
|
return this.client.get<Term>(`${this.basePath}/${id}${queryString}`, headers)
|
|
28
19
|
}
|
|
29
20
|
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { BaseApi } from '../../core/BaseApi'
|
|
2
2
|
import type { HasGetById } from '../../core/types'
|
|
3
3
|
import type { TranscriptResponse } from './types'
|
|
4
4
|
|
|
5
|
-
export class TranscriptsApi implements HasGetById<TranscriptResponse> {
|
|
6
|
-
private readonly client: ApiClient
|
|
5
|
+
export class TranscriptsApi extends BaseApi implements HasGetById<TranscriptResponse> {
|
|
7
6
|
private readonly basePath = '/transcripts'
|
|
8
7
|
|
|
9
|
-
constructor(client: ApiClient) {
|
|
10
|
-
this.client = client
|
|
11
|
-
}
|
|
12
|
-
|
|
13
8
|
/** Triggers audio transcription for a message by dispatching an async job, and marks it as transcribing. */
|
|
14
9
|
getById(id: string, headers?: Record<string, string>): Promise<TranscriptResponse> {
|
|
15
10
|
return this.client.get<TranscriptResponse>(`${this.basePath}/${id}`, headers)
|
package/src/core/ApiClient.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type { QueryFormat } from './queryString'
|
|
2
|
+
|
|
1
3
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
2
4
|
|
|
3
5
|
export interface ApiClient {
|
|
6
|
+
/** How list/get queries are serialized in the URL (defaults to `'json'` when absent). */
|
|
7
|
+
readonly queryFormat?: QueryFormat
|
|
4
8
|
setAccessToken(accessToken: string): this
|
|
5
9
|
request<TResponse = unknown, TBody = unknown>(
|
|
6
10
|
method: HttpMethod,
|
|
@@ -20,8 +20,6 @@ export class ArchivableCrudApi<
|
|
|
20
20
|
> extends BaseCrudApi<TResponse, TCreate, TUpdate, TQuery> {
|
|
21
21
|
constructor(client: ApiClient, basePath: string) {
|
|
22
22
|
super(client, basePath)
|
|
23
|
-
this.archive = this.archive.bind(this)
|
|
24
|
-
this.unarchive = this.unarchive.bind(this)
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
/**
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import type { ApiClient } from './ApiClient'
|
|
3
|
+
import { BaseApi } from './BaseApi'
|
|
4
|
+
import { BaseCrudApi } from './BaseCrudApi'
|
|
5
|
+
import { ArchivableCrudApi } from './ArchivableCrudApi'
|
|
6
|
+
import { bindMethods } from './bindMethods'
|
|
7
|
+
import { AuthHistoryApi } from '../apis/authHistory/AuthHistoryApi'
|
|
8
|
+
import { ClientFeedbackApi } from '../apis/clientFeedback/ClientFeedbackApi'
|
|
9
|
+
import { MeApi } from '../apis/me/MeApi'
|
|
10
|
+
import { ServiceAccessManagementApi } from '../apis/serviceAccessManagement/ServiceAccessManagementApi'
|
|
11
|
+
import { StickerUsersApi } from '../apis/stickerUsers/StickerUsersApi'
|
|
12
|
+
import { TermsApi } from '../apis/terms/TermsApi'
|
|
13
|
+
import { TranscriptsApi } from '../apis/transcripts/TranscriptsApi'
|
|
14
|
+
|
|
15
|
+
function createMockClient(): ApiClient {
|
|
16
|
+
return {
|
|
17
|
+
setAccessToken: vi.fn().mockReturnThis(),
|
|
18
|
+
request: vi.fn(),
|
|
19
|
+
get: vi.fn().mockResolvedValue([]),
|
|
20
|
+
post: vi.fn().mockResolvedValue({}),
|
|
21
|
+
put: vi.fn().mockResolvedValue({}),
|
|
22
|
+
patch: vi.fn().mockResolvedValue({}),
|
|
23
|
+
delete: vi.fn().mockResolvedValue({}),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── bindMethods ──────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe('bindMethods', () => {
|
|
30
|
+
class Parent {
|
|
31
|
+
name = 'instance'
|
|
32
|
+
greet() {
|
|
33
|
+
return `parent:${this.name}`
|
|
34
|
+
}
|
|
35
|
+
shout() {
|
|
36
|
+
return this.greet().toUpperCase()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class Child extends Parent {
|
|
41
|
+
getterCalls = 0
|
|
42
|
+
override greet() {
|
|
43
|
+
return `child:${this.name}`
|
|
44
|
+
}
|
|
45
|
+
get computed() {
|
|
46
|
+
this.getterCalls += 1
|
|
47
|
+
return this.name
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it('binds methods of the whole class chain so they survive destructuring', () => {
|
|
52
|
+
const child = new Child()
|
|
53
|
+
bindMethods(child)
|
|
54
|
+
const { greet, shout } = child
|
|
55
|
+
expect(greet()).toBe('child:instance')
|
|
56
|
+
expect(shout()).toBe('CHILD:INSTANCE')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('binds the most derived implementation', () => {
|
|
60
|
+
const child = new Child()
|
|
61
|
+
bindMethods(child)
|
|
62
|
+
expect(Object.getOwnPropertyDescriptor(child, 'greet')?.value()).toBe('child:instance')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('skips getters without calling them, and constructor', () => {
|
|
66
|
+
const child = new Child()
|
|
67
|
+
bindMethods(child)
|
|
68
|
+
expect(child.getterCalls).toBe(0)
|
|
69
|
+
expect(Object.hasOwn(child, 'computed')).toBe(false)
|
|
70
|
+
expect(Object.hasOwn(child, 'constructor')).toBe(false)
|
|
71
|
+
expect(child.computed).toBe('instance')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('does not bind Object.prototype methods', () => {
|
|
75
|
+
const child = new Child()
|
|
76
|
+
bindMethods(child)
|
|
77
|
+
expect(Object.hasOwn(child, 'toString')).toBe(false)
|
|
78
|
+
expect(Object.hasOwn(child, 'hasOwnProperty')).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// ── BaseApi ──────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
describe('BaseApi', () => {
|
|
85
|
+
class ItemsApi extends BaseApi {
|
|
86
|
+
list(query?: object) {
|
|
87
|
+
return this.client.get(`/items${this.toQueryString(query)}`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
it('exposes the client', () => {
|
|
92
|
+
const client = createMockClient()
|
|
93
|
+
expect(new ItemsApi(client).client).toBe(client)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('binds subclass methods and its own', async () => {
|
|
97
|
+
const client = createMockClient()
|
|
98
|
+
const { list, withQueryFormat } = new ItemsApi(client)
|
|
99
|
+
await list({ page: 1 })
|
|
100
|
+
await withQueryFormat('qs').list({ page: 1 })
|
|
101
|
+
expect(vi.mocked(client.get).mock.calls.map(([url]) => decodeURIComponent(url))).toEqual([
|
|
102
|
+
'/items?query={"page":1}',
|
|
103
|
+
'/items?page=1',
|
|
104
|
+
])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("serializes queries in the client's queryFormat", async () => {
|
|
108
|
+
const client = Object.assign(createMockClient(), { queryFormat: 'qs' as const })
|
|
109
|
+
await new ItemsApi(client).list({ where: { name: 'x' } })
|
|
110
|
+
expect(decodeURIComponent(vi.mocked(client.get).mock.calls[0]?.[0] as string)).toBe(
|
|
111
|
+
'/items?where[name]=x',
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('is the base of BaseCrudApi and ArchivableCrudApi', () => {
|
|
116
|
+
const client = createMockClient()
|
|
117
|
+
expect(new BaseCrudApi(client, '/items')).toBeInstanceOf(BaseApi)
|
|
118
|
+
expect(new ArchivableCrudApi(client, '/items')).toBeInstanceOf(BaseApi)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('binds ArchivableCrudApi methods without manual binding', async () => {
|
|
122
|
+
const client = createMockClient()
|
|
123
|
+
const { archive, unarchive } = new ArchivableCrudApi(client, '/items')
|
|
124
|
+
await archive('1')
|
|
125
|
+
await unarchive('1')
|
|
126
|
+
expect(client.put).toHaveBeenNthCalledWith(1, '/items/1', expect.anything(), undefined)
|
|
127
|
+
expect(client.put).toHaveBeenNthCalledWith(2, '/items/1', { archivedAt: null }, undefined)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// ── APIs that don't extend BaseCrudApi ───────────────────────────────
|
|
132
|
+
|
|
133
|
+
describe.each([
|
|
134
|
+
['AuthHistoryApi', (c: ApiClient) => new AuthHistoryApi(c).getMany],
|
|
135
|
+
['ClientFeedbackApi', (c: ApiClient) => new ClientFeedbackApi(c).create],
|
|
136
|
+
['MeApi', (c: ApiClient) => new MeApi(c).getMe],
|
|
137
|
+
['ServiceAccessManagementApi', (c: ApiClient) => new ServiceAccessManagementApi(c).getMany],
|
|
138
|
+
['StickerUsersApi', (c: ApiClient) => new StickerUsersApi(c).getMany],
|
|
139
|
+
['TermsApi', (c: ApiClient) => new TermsApi(c).getNotAgreed],
|
|
140
|
+
['TranscriptsApi', (c: ApiClient) => new TranscriptsApi(c).getById],
|
|
141
|
+
])('%s', (_name, destructure) => {
|
|
142
|
+
it('extends BaseApi and keeps methods working when destructured', async () => {
|
|
143
|
+
const client = createMockClient()
|
|
144
|
+
const method = destructure(client) as (...args: unknown[]) => Promise<unknown>
|
|
145
|
+
await expect(method('id-1')).resolves.toBeDefined()
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('every API class that does not extend BaseCrudApi extends BaseApi', () => {
|
|
150
|
+
const client = createMockClient()
|
|
151
|
+
for (const api of [
|
|
152
|
+
new AuthHistoryApi(client),
|
|
153
|
+
new ClientFeedbackApi(client),
|
|
154
|
+
new MeApi(client),
|
|
155
|
+
new ServiceAccessManagementApi(client),
|
|
156
|
+
new StickerUsersApi(client),
|
|
157
|
+
new TermsApi(client),
|
|
158
|
+
new TranscriptsApi(client),
|
|
159
|
+
]) {
|
|
160
|
+
expect(api).toBeInstanceOf(BaseApi)
|
|
161
|
+
expect(api.client).toBe(client)
|
|
162
|
+
expect(api.withQueryFormat('qs')).toBeInstanceOf(api.constructor)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ApiClient } from './ApiClient'
|
|
2
|
+
import { bindMethods } from './bindMethods'
|
|
3
|
+
import { toQueryString, type QueryFormat } from './queryString'
|
|
4
|
+
import { cloneWithQueryFormat } from './withQueryFormat'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base of every API class: holds the client, binds the methods to the instance (so they can be
|
|
8
|
+
* destructured) and serializes queries in the client's `queryFormat`.
|
|
9
|
+
*/
|
|
10
|
+
export class BaseApi {
|
|
11
|
+
public readonly client: ApiClient
|
|
12
|
+
|
|
13
|
+
constructor(client: ApiClient) {
|
|
14
|
+
this.client = client
|
|
15
|
+
bindMethods(this)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns a copy of this API whose requests serialize queries in `format`, overriding the
|
|
20
|
+
* client's `queryFormat`. This instance is not changed.
|
|
21
|
+
*
|
|
22
|
+
* @example await contacts.withQueryFormat('qs').getMany({ where: { name: 'x' } })
|
|
23
|
+
*/
|
|
24
|
+
withQueryFormat(format: QueryFormat): this {
|
|
25
|
+
return cloneWithQueryFormat(this, format)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Serializes a list/get query in the client's `queryFormat` (`''` when there is nothing to send). */
|
|
29
|
+
protected toQueryString(query?: object | null): string {
|
|
30
|
+
return toQueryString(query, this.client.queryFormat)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -54,6 +54,16 @@ describe('BaseApiClient', () => {
|
|
|
54
54
|
expect(spy).toHaveBeenCalledWith('https://api.example.com/test', expect.anything())
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
+
it("defaults queryFormat to 'json'", () => {
|
|
58
|
+
expect(new BaseApiClient('https://api.example.com').queryFormat).toBe('json')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('accepts a queryFormat option', () => {
|
|
62
|
+
expect(
|
|
63
|
+
new BaseApiClient('https://api.example.com', 'tok', { queryFormat: 'qs' }).queryFormat,
|
|
64
|
+
).toBe('qs')
|
|
65
|
+
})
|
|
66
|
+
|
|
57
67
|
// ── setAccessToken ───────────────────────────────────────────────
|
|
58
68
|
|
|
59
69
|
it('returns this for chaining', () => {
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import type { ApiClient, HttpMethod } from './ApiClient'
|
|
2
2
|
import { ApiConnectionError, ApiError } from './errors'
|
|
3
|
+
import { DEFAULT_QUERY_FORMAT, type QueryFormat } from './queryString'
|
|
4
|
+
|
|
5
|
+
export interface BaseApiClientOptions {
|
|
6
|
+
/** How list/get queries are serialized in the URL. Defaults to `'json'`. */
|
|
7
|
+
queryFormat?: QueryFormat
|
|
8
|
+
}
|
|
3
9
|
|
|
4
10
|
export class BaseApiClient implements ApiClient {
|
|
5
11
|
protected readonly baseUrl: string
|
|
6
12
|
protected accessToken: string | null = null
|
|
13
|
+
public readonly queryFormat: QueryFormat
|
|
7
14
|
|
|
8
|
-
constructor(baseUrl: string, accessToken?: string) {
|
|
15
|
+
constructor(baseUrl: string, accessToken?: string, options: BaseApiClientOptions = {}) {
|
|
9
16
|
this.baseUrl = baseUrl.replace(/\/$/, '')
|
|
10
17
|
this.accessToken = accessToken || null
|
|
18
|
+
this.queryFormat = options.queryFormat ?? DEFAULT_QUERY_FORMAT
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
setAccessToken(accessToken: string): this {
|