@ikatec/digisac-api-sdk 4.0.0 → 4.2.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.
Files changed (224) hide show
  1. package/README.md +34 -4
  2. package/dist/apis/authHistory/AuthHistoryApi.cjs +11 -17
  3. package/dist/apis/authHistory/AuthHistoryApi.cjs.map +1 -1
  4. package/dist/apis/authHistory/AuthHistoryApi.d.ts +10 -18
  5. package/dist/apis/authHistory/AuthHistoryApi.d.ts.map +1 -1
  6. package/dist/apis/authHistory/AuthHistoryApi.mjs +11 -15
  7. package/dist/apis/authHistory/AuthHistoryApi.mjs.map +1 -1
  8. package/dist/apis/authHistory/types.d.ts +4 -3
  9. package/dist/apis/authHistory/types.d.ts.map +1 -1
  10. package/dist/apis/clientFeedback/ClientFeedbackApi.cjs +2 -5
  11. package/dist/apis/clientFeedback/ClientFeedbackApi.cjs.map +1 -1
  12. package/dist/apis/clientFeedback/ClientFeedbackApi.d.ts +2 -4
  13. package/dist/apis/clientFeedback/ClientFeedbackApi.d.ts.map +1 -1
  14. package/dist/apis/clientFeedback/ClientFeedbackApi.mjs +3 -5
  15. package/dist/apis/clientFeedback/ClientFeedbackApi.mjs.map +1 -1
  16. package/dist/apis/contacts/ContactsApi.cjs +2 -2
  17. package/dist/apis/contacts/ContactsApi.cjs.map +1 -1
  18. package/dist/apis/contacts/ContactsApi.mjs +2 -2
  19. package/dist/apis/contacts/ContactsApi.mjs.map +1 -1
  20. package/dist/apis/customFields/types.d.ts +21 -19
  21. package/dist/apis/customFields/types.d.ts.map +1 -1
  22. package/dist/apis/dashboard/DashboardApi.cjs +19 -0
  23. package/dist/apis/dashboard/DashboardApi.cjs.map +1 -0
  24. package/dist/apis/dashboard/DashboardApi.d.ts +12 -0
  25. package/dist/apis/dashboard/DashboardApi.d.ts.map +1 -0
  26. package/dist/apis/dashboard/DashboardApi.mjs +18 -0
  27. package/dist/apis/dashboard/DashboardApi.mjs.map +1 -0
  28. package/dist/apis/dashboard/index.cjs +4 -0
  29. package/dist/apis/dashboard/index.d.ts +3 -0
  30. package/dist/apis/dashboard/index.d.ts.map +1 -0
  31. package/dist/apis/dashboard/index.mjs +3 -0
  32. package/dist/apis/dashboard/types.cjs +0 -0
  33. package/dist/apis/dashboard/types.d.ts +53 -0
  34. package/dist/apis/dashboard/types.d.ts.map +1 -0
  35. package/dist/apis/dashboard/types.mjs +0 -0
  36. package/dist/apis/holiday/types.d.ts +34 -8
  37. package/dist/apis/holiday/types.d.ts.map +1 -1
  38. package/dist/apis/index.cjs +6 -0
  39. package/dist/apis/index.d.ts +2 -0
  40. package/dist/apis/index.d.ts.map +1 -1
  41. package/dist/apis/index.mjs +5 -1
  42. package/dist/apis/me/MeApi.cjs +2 -5
  43. package/dist/apis/me/MeApi.cjs.map +1 -1
  44. package/dist/apis/me/MeApi.d.ts +2 -4
  45. package/dist/apis/me/MeApi.d.ts.map +1 -1
  46. package/dist/apis/me/MeApi.mjs +3 -5
  47. package/dist/apis/me/MeApi.mjs.map +1 -1
  48. package/dist/apis/now/NowApi.cjs +37 -0
  49. package/dist/apis/now/NowApi.cjs.map +1 -0
  50. package/dist/apis/now/NowApi.d.ts +26 -0
  51. package/dist/apis/now/NowApi.d.ts.map +1 -0
  52. package/dist/apis/now/NowApi.mjs +36 -0
  53. package/dist/apis/now/NowApi.mjs.map +1 -0
  54. package/dist/apis/now/index.cjs +4 -0
  55. package/dist/apis/now/index.d.ts +3 -0
  56. package/dist/apis/now/index.d.ts.map +1 -0
  57. package/dist/apis/now/index.mjs +3 -0
  58. package/dist/apis/now/types.cjs +0 -0
  59. package/dist/apis/now/types.d.ts +45 -0
  60. package/dist/apis/now/types.d.ts.map +1 -0
  61. package/dist/apis/now/types.mjs +0 -0
  62. package/dist/apis/people/types.d.ts +7 -2
  63. package/dist/apis/people/types.d.ts.map +1 -1
  64. package/dist/apis/permissions/PermissionsApi.cjs +6 -2
  65. package/dist/apis/permissions/PermissionsApi.cjs.map +1 -1
  66. package/dist/apis/permissions/PermissionsApi.d.ts +7 -3
  67. package/dist/apis/permissions/PermissionsApi.d.ts.map +1 -1
  68. package/dist/apis/permissions/PermissionsApi.mjs +6 -2
  69. package/dist/apis/permissions/PermissionsApi.mjs.map +1 -1
  70. package/dist/apis/permissions/types.d.ts +4 -14
  71. package/dist/apis/permissions/types.d.ts.map +1 -1
  72. package/dist/apis/personalAccessTokens/PersonalAccessTokensApi.cjs +5 -0
  73. package/dist/apis/personalAccessTokens/PersonalAccessTokensApi.cjs.map +1 -1
  74. package/dist/apis/personalAccessTokens/PersonalAccessTokensApi.d.ts +5 -0
  75. package/dist/apis/personalAccessTokens/PersonalAccessTokensApi.d.ts.map +1 -1
  76. package/dist/apis/personalAccessTokens/PersonalAccessTokensApi.mjs +5 -0
  77. package/dist/apis/personalAccessTokens/PersonalAccessTokensApi.mjs.map +1 -1
  78. package/dist/apis/personalAccessTokens/types.d.ts +23 -4
  79. package/dist/apis/personalAccessTokens/types.d.ts.map +1 -1
  80. package/dist/apis/roles/RolesApi.cjs.map +1 -1
  81. package/dist/apis/roles/RolesApi.d.ts +2 -2
  82. package/dist/apis/roles/RolesApi.d.ts.map +1 -1
  83. package/dist/apis/roles/RolesApi.mjs.map +1 -1
  84. package/dist/apis/roles/types.d.ts +27 -4
  85. package/dist/apis/roles/types.d.ts.map +1 -1
  86. package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.cjs +3 -9
  87. package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.cjs.map +1 -1
  88. package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.d.ts +3 -4
  89. package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.d.ts.map +1 -1
  90. package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.mjs +3 -7
  91. package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.mjs.map +1 -1
  92. package/dist/apis/stickerUsers/StickerUsersApi.cjs +3 -9
  93. package/dist/apis/stickerUsers/StickerUsersApi.cjs.map +1 -1
  94. package/dist/apis/stickerUsers/StickerUsersApi.d.ts +3 -4
  95. package/dist/apis/stickerUsers/StickerUsersApi.d.ts.map +1 -1
  96. package/dist/apis/stickerUsers/StickerUsersApi.mjs +3 -7
  97. package/dist/apis/stickerUsers/StickerUsersApi.mjs.map +1 -1
  98. package/dist/apis/terms/TermsApi.cjs +7 -13
  99. package/dist/apis/terms/TermsApi.cjs.map +1 -1
  100. package/dist/apis/terms/TermsApi.d.ts +6 -5
  101. package/dist/apis/terms/TermsApi.d.ts.map +1 -1
  102. package/dist/apis/terms/TermsApi.mjs +7 -11
  103. package/dist/apis/terms/TermsApi.mjs.map +1 -1
  104. package/dist/apis/timetable/types.d.ts +12 -7
  105. package/dist/apis/timetable/types.d.ts.map +1 -1
  106. package/dist/apis/transcripts/TranscriptsApi.cjs +2 -5
  107. package/dist/apis/transcripts/TranscriptsApi.cjs.map +1 -1
  108. package/dist/apis/transcripts/TranscriptsApi.d.ts +2 -4
  109. package/dist/apis/transcripts/TranscriptsApi.d.ts.map +1 -1
  110. package/dist/apis/transcripts/TranscriptsApi.mjs +3 -5
  111. package/dist/apis/transcripts/TranscriptsApi.mjs.map +1 -1
  112. package/dist/core/ApiClient.d.ts +3 -0
  113. package/dist/core/ApiClient.d.ts.map +1 -1
  114. package/dist/core/ArchivableCrudApi.cjs +0 -2
  115. package/dist/core/ArchivableCrudApi.cjs.map +1 -1
  116. package/dist/core/ArchivableCrudApi.d.ts.map +1 -1
  117. package/dist/core/ArchivableCrudApi.mjs +0 -2
  118. package/dist/core/ArchivableCrudApi.mjs.map +1 -1
  119. package/dist/core/BaseApi.cjs +34 -0
  120. package/dist/core/BaseApi.cjs.map +1 -0
  121. package/dist/core/BaseApi.d.ts +20 -0
  122. package/dist/core/BaseApi.d.ts.map +1 -0
  123. package/dist/core/BaseApi.mjs +33 -0
  124. package/dist/core/BaseApi.mjs.map +1 -0
  125. package/dist/core/BaseApiClient.cjs +4 -1
  126. package/dist/core/BaseApiClient.cjs.map +1 -1
  127. package/dist/core/BaseApiClient.d.ts +7 -1
  128. package/dist/core/BaseApiClient.d.ts.map +1 -1
  129. package/dist/core/BaseApiClient.mjs +4 -1
  130. package/dist/core/BaseApiClient.mjs.map +1 -1
  131. package/dist/core/BaseCrudApi.cjs +3 -40
  132. package/dist/core/BaseCrudApi.cjs.map +1 -1
  133. package/dist/core/BaseCrudApi.d.ts +4 -23
  134. package/dist/core/BaseCrudApi.d.ts.map +1 -1
  135. package/dist/core/BaseCrudApi.mjs +3 -38
  136. package/dist/core/BaseCrudApi.mjs.map +1 -1
  137. package/dist/core/BaseReadApi.cjs +44 -0
  138. package/dist/core/BaseReadApi.cjs.map +1 -0
  139. package/dist/core/BaseReadApi.d.ts +32 -0
  140. package/dist/core/BaseReadApi.d.ts.map +1 -0
  141. package/dist/core/BaseReadApi.mjs +43 -0
  142. package/dist/core/BaseReadApi.mjs.map +1 -0
  143. package/dist/core/bindMethods.cjs +24 -0
  144. package/dist/core/bindMethods.cjs.map +1 -0
  145. package/dist/core/bindMethods.d.ts +7 -0
  146. package/dist/core/bindMethods.d.ts.map +1 -0
  147. package/dist/core/bindMethods.mjs +22 -0
  148. package/dist/core/bindMethods.mjs.map +1 -0
  149. package/dist/core/index.cjs +4 -0
  150. package/dist/core/index.d.ts +4 -0
  151. package/dist/core/index.d.ts.map +1 -1
  152. package/dist/core/index.mjs +3 -1
  153. package/dist/core/queryString.cjs +22 -0
  154. package/dist/core/queryString.cjs.map +1 -0
  155. package/dist/core/queryString.d.ts +14 -0
  156. package/dist/core/queryString.d.ts.map +1 -0
  157. package/dist/core/queryString.mjs +18 -0
  158. package/dist/core/queryString.mjs.map +1 -0
  159. package/dist/core/types.d.ts +2 -0
  160. package/dist/core/types.d.ts.map +1 -1
  161. package/dist/core/withQueryFormat.cjs +42 -0
  162. package/dist/core/withQueryFormat.cjs.map +1 -0
  163. package/dist/core/withQueryFormat.d.ts +10 -0
  164. package/dist/core/withQueryFormat.d.ts.map +1 -0
  165. package/dist/core/withQueryFormat.mjs +40 -0
  166. package/dist/core/withQueryFormat.mjs.map +1 -0
  167. package/dist/index.cjs +4 -0
  168. package/dist/index.mjs +3 -1
  169. package/package.json +1 -1
  170. package/src/apis/authHistory/AuthHistoryApi.test.ts +11 -3
  171. package/src/apis/authHistory/AuthHistoryApi.ts +11 -37
  172. package/src/apis/authHistory/types.ts +4 -3
  173. package/src/apis/campaigns/CampaignsApi.test.ts +1 -1
  174. package/src/apis/clientFeedback/ClientFeedbackApi.ts +5 -7
  175. package/src/apis/contacts/ContactsApi.test.ts +1 -1
  176. package/src/apis/contacts/ContactsApi.ts +2 -2
  177. package/src/apis/customFields/types.ts +21 -25
  178. package/src/apis/dashboard/DashboardApi.test.ts +82 -0
  179. package/src/apis/dashboard/DashboardApi.ts +18 -0
  180. package/src/apis/dashboard/index.ts +2 -0
  181. package/src/apis/dashboard/types.ts +53 -0
  182. package/src/apis/holiday/types.ts +36 -8
  183. package/src/apis/index.ts +2 -0
  184. package/src/apis/me/MeApi.ts +2 -8
  185. package/src/apis/messages/MessagesApi.test.ts +2 -2
  186. package/src/apis/now/NowApi.test.ts +125 -0
  187. package/src/apis/now/NowApi.ts +59 -0
  188. package/src/apis/now/index.ts +2 -0
  189. package/src/apis/now/types.ts +47 -0
  190. package/src/apis/payloads.test-d.ts +39 -0
  191. package/src/apis/payloads.test.ts +81 -0
  192. package/src/apis/people/types.ts +7 -2
  193. package/src/apis/permissions/PermissionsApi.test.ts +93 -0
  194. package/src/apis/permissions/PermissionsApi.ts +7 -7
  195. package/src/apis/permissions/types.ts +4 -16
  196. package/src/apis/personalAccessTokens/PersonalAccessTokensApi.ts +5 -0
  197. package/src/apis/personalAccessTokens/types.ts +25 -4
  198. package/src/apis/queryFormat.test.ts +273 -0
  199. package/src/apis/roles/RolesApi.ts +7 -2
  200. package/src/apis/roles/types.ts +27 -4
  201. package/src/apis/serviceAccessManagement/ServiceAccessManagementApi.test.ts +2 -2
  202. package/src/apis/serviceAccessManagement/ServiceAccessManagementApi.ts +10 -10
  203. package/src/apis/stickerUsers/StickerUsersApi.test.ts +1 -1
  204. package/src/apis/stickerUsers/StickerUsersApi.ts +7 -10
  205. package/src/apis/terms/TermsApi.ts +7 -13
  206. package/src/apis/timetable/types.ts +13 -7
  207. package/src/apis/transcripts/TranscriptsApi.ts +2 -7
  208. package/src/core/ApiClient.ts +4 -0
  209. package/src/core/ArchivableCrudApi.ts +0 -2
  210. package/src/core/BaseApi.test.ts +164 -0
  211. package/src/core/BaseApi.ts +32 -0
  212. package/src/core/BaseApiClient.test.ts +10 -0
  213. package/src/core/BaseApiClient.ts +9 -1
  214. package/src/core/BaseCrudApi.test-d.ts +9 -0
  215. package/src/core/BaseCrudApi.test.ts +131 -60
  216. package/src/core/BaseCrudApi.ts +7 -63
  217. package/src/core/BaseReadApi.ts +70 -0
  218. package/src/core/bindMethods.ts +26 -0
  219. package/src/core/index.ts +4 -0
  220. package/src/core/queryString.test.ts +71 -0
  221. package/src/core/queryString.ts +30 -0
  222. package/src/core/types.ts +5 -0
  223. package/src/core/withQueryFormat.test.ts +136 -0
  224. package/src/core/withQueryFormat.ts +47 -0
@@ -1,5 +1,9 @@
1
+ import type { ListQuery } from '../../core/types'
1
2
  import type { Permission } from '../permissions/types'
2
3
 
4
+ /**
5
+ * A role (cargo). Roles are not soft-deleted, so there is no `deletedAt`.
6
+ */
3
7
  export type Role = {
4
8
  id: string
5
9
  displayName: string
@@ -7,21 +11,40 @@ export type Role = {
7
11
  accountId: string
8
12
  createdAt: string
9
13
  updatedAt: string
10
- deletedAt: string | null
14
+ /** Only when listed with `customInclude: ['usersCount']`. */
15
+ usersCount?: number
11
16
  // relationships
12
17
  permissions?: Permission[]
13
18
  }
14
19
 
15
20
  export type RoleRelationships = 'permissions'
16
21
 
22
+ /**
23
+ * The permissions of a role, by id or `{ id }`. The backend reads the `permissions` key (also
24
+ * `permissionsId`/`permissionsIds`) — any other key is silently ignored and the role ends up with
25
+ * no permissions.
26
+ */
27
+ export type RolePermissionRef = string | { id: string }
28
+
17
29
  export type CreateRolePayload = {
30
+ /** Up to 255 characters; must be unique in the account. */
18
31
  displayName: string
19
32
  isAdmin?: boolean
20
- permissionIds?: string[]
33
+ permissions?: RolePermissionRef[]
21
34
  }
22
35
 
23
36
  export type UpdateRolePayload = {
24
- displayName?: string
37
+ /** Required by the backend on every update (400 `required` otherwise). */
38
+ displayName: string
25
39
  isAdmin?: boolean | null
26
- permissionIds?: string[]
40
+ /** Replaces the role permissions. An empty array leaves them unchanged. */
41
+ permissions?: RolePermissionRef[]
42
+ }
43
+
44
+ /** List query of `/roles`, with the backend extras to count the users of each role. */
45
+ export type RolesListQuery = ListQuery<Role> & {
46
+ /** `['usersCount']` adds `usersCount` to each role. */
47
+ customInclude?: 'usersCount'[]
48
+ /** Which users `usersCount` counts (default `all`). */
49
+ usersCountFilter?: 'all' | 'active' | 'archived'
27
50
  }
@@ -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=2')
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=false')
80
+ expect(decodeURIComponent(url)).toContain('"paginate":false')
81
81
  expect(result).toEqual([record])
82
82
  })
83
83
 
@@ -1,15 +1,11 @@
1
- import qs from 'qs'
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 implements HasGetMany<ServiceAccessManagement> {
7
- private readonly client: ApiClient
8
-
9
- constructor(client: ApiClient) {
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 = query ? `?${qs.stringify(query)}` : ''
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=2')
69
+ expect(decodeURIComponent(url)).toContain('"page":2')
70
70
  })
71
71
 
72
72
  it('forwards custom headers', async () => {
@@ -1,15 +1,8 @@
1
- import qs from 'qs'
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 = query ? `?${qs.stringify(query)}` : ''
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 qs from 'qs'
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,15 @@ 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
- /** Returns a single term by its ID, scoped to the current account (`null` when not found). */
15
+ /**
16
+ * Returns a single term by its ID, scoped to the current account. When it does not exist the
17
+ * backend answers 200 with an empty body, which resolves to `undefined`.
18
+ */
25
19
  getById(id: string, query?: GetByIdQuery<Term>, headers?: Record<string, string>): Promise<Term> {
26
- const queryString = query ? `?${qs.stringify(query)}` : ''
20
+ const queryString = this.toQueryString(query)
27
21
  return this.client.get<Term>(`${this.basePath}/${id}${queryString}`, headers)
28
22
  }
29
23
 
@@ -1,8 +1,15 @@
1
+ export type TimetableWeekDay = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
2
+
3
+ /**
4
+ * A working period of the timetable. `start`/`end` are `HH:MM` and `end` must be later than
5
+ * `start`; the period applies to every day in `weekDays`.
6
+ */
1
7
  export type TimetableWorkSlot = {
2
- day: number
8
+ /** `HH:MM` */
3
9
  start: string
10
+ /** `HH:MM`, later than `start`. */
4
11
  end: string
5
- active: boolean
12
+ weekDays: TimetableWeekDay[]
6
13
  }
7
14
 
8
15
  export type Timetable = {
@@ -16,13 +23,12 @@ export type Timetable = {
16
23
  }
17
24
 
18
25
  export type CreateTimetablePayload = {
26
+ /** Must be unique in the account (409 otherwise, unless the account allows duplicate names). */
19
27
  name: string
28
+ /** Minutes before the end of the period to notify. Must be truthy: `0` answers 400 `required`. */
20
29
  previousTimeToNotification: number
21
30
  workPlan: TimetableWorkSlot[]
22
31
  }
23
32
 
24
- export type UpdateTimetablePayload = {
25
- name?: string
26
- previousTimeToNotification?: number
27
- workPlan?: TimetableWorkSlot[]
28
- }
33
+ /** The backend requires the three fields on every update (400 `required` otherwise). */
34
+ export type UpdateTimetablePayload = CreateTimetablePayload
@@ -1,15 +1,10 @@
1
- import type { ApiClient } from '../../core/ApiClient'
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)
@@ -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 {
@@ -20,6 +20,9 @@ expectType<Promise<Paginated<Item>>>(api.getMany({ paginate: true }))
20
20
  expectType<Promise<Item[]>>(api.getMany({ paginate: false }))
21
21
  expectType<Promise<Item[]>>(api.getMany({ where: { name: 'x' }, paginate: false }))
22
22
 
23
+ declare const paginate: boolean
24
+ expectType<Promise<Paginated<Item> | Item[]>>(api.getMany({ paginate }))
25
+
23
26
  // ── getOne / getById ────────────────────────────────────────────────
24
27
 
25
28
  expectType<Promise<Item | null>>(api.getOne())
@@ -46,3 +49,9 @@ expectError(api.updateById('1', { name: 1 }))
46
49
  expectError(api.getMany({ where: { missing: 'x' } }))
47
50
  expectError(api.getMany({ order: [['missing', 'ASC']] }))
48
51
  expectError(api.getMany({ include: ['name'] }))
52
+
53
+ // ── withQueryFormat ─────────────────────────────────────────────────
54
+
55
+ expectType<typeof api>(api.withQueryFormat('qs'))
56
+ expectType<Promise<Item[]>>(api.withQueryFormat('json').getMany({ paginate: false }))
57
+ expectError(api.withQueryFormat('xml'))