@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
@@ -0,0 +1,47 @@
1
+ import type { User } from '../users/types'
2
+
3
+ /**
4
+ * Filters of the "Agora" (now) overview. Each accepts one id, an array or a comma-separated list.
5
+ * A user with only `overview.now.view.departments` gets 403 for departments/users outside their
6
+ * departments.
7
+ */
8
+ export type NowQuery = {
9
+ departmentId?: string | string[]
10
+ userId?: string | string[]
11
+ }
12
+
13
+ /** Every `/now` route answers its data under `totals`. */
14
+ export type NowResponse<T> = { totals: T }
15
+
16
+ export type NowResumeTotals = {
17
+ openTickets: number
18
+ queueTickets: number
19
+ usersWithAttendance: number
20
+ usersWithoutAttendance: number
21
+ /** Postgres `avg()` — a numeric string, or `0` when there is no data. */
22
+ averageOpenTicketsTime: number | string
23
+ /** Postgres `avg()` — a numeric string, or `0` when there is no data. */
24
+ averageOpenTicketsWaitTime: number | string
25
+ onlineUsersCount: number
26
+ offlineUsersCount: number
27
+ absentUsersCount: number
28
+ }
29
+
30
+ /** One entry per department. */
31
+ export type NowDepartmentResume = {
32
+ name: string
33
+ onlineUserCount: number
34
+ offlineUserCount: number
35
+ absentUserCount: number
36
+ onQueueTicketCount: number
37
+ openTicketCount: number
38
+ }
39
+
40
+ /** One entry per attendant (user). */
41
+ export type NowAttendanceResume = {
42
+ name?: string
43
+ /** Department names — `0` when the attendant has no department. */
44
+ departments: string[] | 0
45
+ status?: User['status']
46
+ ticketsOpenCount: number
47
+ }
@@ -0,0 +1,39 @@
1
+ import { expectError } from 'tsd'
2
+ import type { ApiClient } from '../core/ApiClient'
3
+ import { PersonalAccessTokensApi } from './personalAccessTokens/PersonalAccessTokensApi'
4
+ import { RolesApi } from './roles/RolesApi'
5
+ import { TimetableApi } from './timetable/TimetableApi'
6
+
7
+ declare const client: ApiClient
8
+
9
+ // ── roles: the backend drops `permissionIds` in silence ─────────────
10
+
11
+ const roles = new RolesApi(client)
12
+ roles.create({ displayName: 'Supervisor', permissions: ['p-1', { id: 'p-2' }] })
13
+ expectError(roles.create({ displayName: 'Supervisor', permissionIds: ['p-1'] }))
14
+ expectError(roles.updateById('r-1', { permissions: ['p-1'] }))
15
+
16
+ // ── timetable: the old workPlan shape answers 400 ───────────────────
17
+
18
+ const timetable = new TimetableApi(client)
19
+ timetable.create({
20
+ name: 'Business hours',
21
+ previousTimeToNotification: 15,
22
+ workPlan: [{ start: '08:00', end: '18:00', weekDays: ['mon'] }],
23
+ })
24
+ expectError(
25
+ timetable.create({
26
+ name: 'Business hours',
27
+ previousTimeToNotification: 15,
28
+ workPlan: [{ day: 1, start: '08:00', end: '18:00', active: true }],
29
+ }),
30
+ )
31
+ expectError(timetable.updateById('t-1', { name: 'Business hours' }))
32
+
33
+ // ── personal access tokens: `expiresIn` is the only lifetime accepted ─
34
+
35
+ const tokens = new PersonalAccessTokensApi(client)
36
+ tokens.create({ name: 'CI', expiresIn: '90d' })
37
+ expectError(tokens.create({ name: 'CI', accessTokenExpiresAt: '2030-01-01T00:00:00Z' }))
38
+ expectError(tokens.create({ name: 'CI', expiresIn: '7d' }))
39
+ expectError(tokens.updateById('t-1', { scope: '*' }))
@@ -0,0 +1,81 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ApiClient } from '../core/ApiClient'
3
+ import { PersonalAccessTokensApi } from './personalAccessTokens/PersonalAccessTokensApi'
4
+ import { RolesApi } from './roles/RolesApi'
5
+ import { TimetableApi } from './timetable/TimetableApi'
6
+
7
+ // Pins the body keys the backend reads: a key it does not know is dropped in silence, so a
8
+ // regression would only show up as missing data (e.g. a role created without permissions).
9
+
10
+ // ── Mock client factory ──────────────────────────────────────────────
11
+
12
+ function createMockClient(): ApiClient {
13
+ return {
14
+ setAccessToken: vi.fn().mockReturnThis(),
15
+ request: vi.fn(),
16
+ get: vi.fn(),
17
+ post: vi.fn(),
18
+ put: vi.fn(),
19
+ patch: vi.fn(),
20
+ delete: vi.fn(),
21
+ }
22
+ }
23
+
24
+ // ── Tests ────────────────────────────────────────────────────────────
25
+
26
+ describe('payloads sent to the backend', () => {
27
+ let client: ReturnType<typeof createMockClient>
28
+
29
+ beforeEach(() => {
30
+ client = createMockClient()
31
+ })
32
+
33
+ describe('RolesApi', () => {
34
+ it('create sends the permissions under `permissions`', async () => {
35
+ await new RolesApi(client).create({
36
+ displayName: 'Supervisor',
37
+ permissions: ['p-1', { id: 'p-2' }],
38
+ })
39
+ expect(client.post).toHaveBeenCalledWith(
40
+ '/roles',
41
+ { displayName: 'Supervisor', permissions: ['p-1', { id: 'p-2' }] },
42
+ undefined,
43
+ )
44
+ })
45
+
46
+ it('updateById sends displayName and the permissions under `permissions`', async () => {
47
+ await new RolesApi(client).updateById('r-1', {
48
+ displayName: 'Supervisor',
49
+ permissions: ['p-1'],
50
+ })
51
+ expect(client.put).toHaveBeenCalledWith(
52
+ '/roles/r-1',
53
+ { displayName: 'Supervisor', permissions: ['p-1'] },
54
+ undefined,
55
+ )
56
+ })
57
+ })
58
+
59
+ describe('TimetableApi', () => {
60
+ it('create sends the workPlan as { start, end, weekDays }', async () => {
61
+ const body = {
62
+ name: 'Business hours',
63
+ previousTimeToNotification: 15,
64
+ workPlan: [{ start: '08:00', end: '18:00', weekDays: ['mon' as const, 'fri' as const] }],
65
+ }
66
+ await new TimetableApi(client).create(body)
67
+ expect(client.post).toHaveBeenCalledWith('/timetable', body, undefined)
68
+ })
69
+ })
70
+
71
+ describe('PersonalAccessTokensApi', () => {
72
+ it('create sends the lifetime under `expiresIn`', async () => {
73
+ await new PersonalAccessTokensApi(client).create({ name: 'CI', expiresIn: '90d' })
74
+ expect(client.post).toHaveBeenCalledWith(
75
+ '/me/tokens',
76
+ { name: 'CI', expiresIn: '90d' },
77
+ undefined,
78
+ )
79
+ })
80
+ })
81
+ })
@@ -17,10 +17,15 @@ export type PersonRelationships = 'organizations' | 'contacts'
17
17
 
18
18
  export type CreatePersonPayload = {
19
19
  name: string
20
- document?: string
20
+ document?: string | null
21
+ /** Organizations of the account to link the person to (400 if any belongs to another account). */
22
+ organizationIds?: string[]
21
23
  }
22
24
 
23
25
  export type UpdatePersonPayload = {
24
- name?: string
26
+ /** Required by the backend on every update (400 `required` otherwise). */
27
+ name: string
25
28
  document?: string | null
29
+ /** Replaces the linked organizations: `[]` unlinks all, omitting it keeps them. */
30
+ organizationIds?: string[]
26
31
  }
@@ -0,0 +1,93 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { PermissionsApi } from './PermissionsApi'
3
+ import type { ApiClient } from '../../core/ApiClient'
4
+ import type { Permission } 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
+ // ── Fixtures ─────────────────────────────────────────────────────────
21
+
22
+ const permission: Permission = { id: 'p-1', name: 'customFields.create', type: 'customFields' }
23
+
24
+ // ── Tests ────────────────────────────────────────────────────────────
25
+
26
+ describe('PermissionsApi', () => {
27
+ let client: ReturnType<typeof createMockClient>
28
+ let api: PermissionsApi
29
+
30
+ beforeEach(() => {
31
+ client = createMockClient()
32
+ api = new PermissionsApi(client)
33
+ })
34
+
35
+ it('exposes only the read routes served by the backend', () => {
36
+ expect(api.basePath).toBe('/permissions')
37
+ expect(api).not.toHaveProperty('create')
38
+ expect(api).not.toHaveProperty('updateById')
39
+ expect(api).not.toHaveProperty('deleteById')
40
+ })
41
+
42
+ describe('getMany', () => {
43
+ it('calls client.get with /permissions and no query string when there is no query', async () => {
44
+ vi.mocked(client.get).mockResolvedValue({ data: [permission], total: 1 })
45
+ await api.getMany()
46
+ expect(client.get).toHaveBeenCalledWith('/permissions', undefined)
47
+ })
48
+
49
+ it('serializes the query in the client format', async () => {
50
+ vi.mocked(client.get).mockResolvedValue([permission])
51
+ const result = await api.getMany({ paginate: false })
52
+ expect(client.get).toHaveBeenCalledWith(
53
+ `/permissions?query=${encodeURIComponent(JSON.stringify({ paginate: false }))}`,
54
+ undefined,
55
+ )
56
+ expect(result).toEqual([permission])
57
+ })
58
+ })
59
+
60
+ describe('getOne', () => {
61
+ it('asks for a single unpaginated result and returns it', async () => {
62
+ vi.mocked(client.get).mockResolvedValue([permission])
63
+ const result = await api.getOne({ where: { name: 'customFields.create' } })
64
+ const query = { where: { name: 'customFields.create' }, limit: 1, paginate: false }
65
+ expect(client.get).toHaveBeenCalledWith(
66
+ `/permissions?query=${encodeURIComponent(JSON.stringify(query))}`,
67
+ undefined,
68
+ )
69
+ expect(result).toEqual(permission)
70
+ })
71
+
72
+ it('returns null when nothing matches', async () => {
73
+ vi.mocked(client.get).mockResolvedValue([])
74
+ expect(await api.getOne()).toBeNull()
75
+ })
76
+ })
77
+
78
+ describe('getById', () => {
79
+ it('calls client.get with /permissions/:id', async () => {
80
+ vi.mocked(client.get).mockResolvedValue(permission)
81
+ const result = await api.getById('p-1')
82
+ expect(client.get).toHaveBeenCalledWith('/permissions/p-1', undefined)
83
+ expect(result).toEqual(permission)
84
+ })
85
+
86
+ it('forwards custom headers', async () => {
87
+ vi.mocked(client.get).mockResolvedValue(permission)
88
+ const headers = { 'X-Custom': 'h' }
89
+ await api.getById('p-1', undefined, headers)
90
+ expect(client.get).toHaveBeenCalledWith('/permissions/p-1', headers)
91
+ })
92
+ })
93
+ })
@@ -1,12 +1,12 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
- import { BaseCrudApi } from '../../core/BaseCrudApi'
3
- import type { Permission, CreatePermissionPayload, UpdatePermissionPayload } from './types'
2
+ import { BaseReadApi } from '../../core/BaseReadApi'
3
+ import type { Permission } from './types'
4
4
 
5
- export class PermissionsApi extends BaseCrudApi<
6
- Permission,
7
- CreatePermissionPayload,
8
- UpdatePermissionPayload
9
- > {
5
+ /**
6
+ * The permission catalog used by roles. The backend only serves `GET /permissions` and
7
+ * `GET /permissions/:id` (guarded by `roles.view`), so there is no create/update/delete.
8
+ */
9
+ export class PermissionsApi extends BaseReadApi<Permission> {
10
10
  constructor(client: ApiClient) {
11
11
  super(client, '/permissions')
12
12
  }
@@ -210,24 +210,12 @@ export type PermissionName =
210
210
  | 'kanban.update'
211
211
  | 'kanban.destroy'
212
212
 
213
+ /**
214
+ * A permission, as `GET /permissions` answers it (the transformer only picks these fields).
215
+ * Permissions are a fixed catalog: the API is read-only.
216
+ */
213
217
  export type Permission = {
214
218
  id: string
215
219
  name: PermissionName
216
220
  type: PermissionType | null
217
- description: string | null
218
- createdAt: string
219
- updatedAt: string
220
- deletedAt: string | null
221
- }
222
-
223
- export type CreatePermissionPayload = {
224
- name: PermissionName
225
- type?: PermissionType
226
- description?: string
227
- }
228
-
229
- export type UpdatePermissionPayload = {
230
- name?: PermissionName
231
- type?: PermissionType | null
232
- description?: string | null
233
221
  }
@@ -6,6 +6,11 @@ import type {
6
6
  UpdatePersonalAccessTokenPayload,
7
7
  } from './types'
8
8
 
9
+ /**
10
+ * Personal access tokens of the authenticated user (`/me/tokens`, all routes guarded by
11
+ * `settingsApi.update`). The listing is scoped to the user; `getById` of a missing token answers
12
+ * 404 (`{ message: 'Not found' }`) instead of `null`.
13
+ */
9
14
  export class PersonalAccessTokensApi extends BaseCrudApi<
10
15
  PersonalAccessToken,
11
16
  CreatePersonalAccessTokenPayload,
@@ -1,28 +1,49 @@
1
1
  import type { User } from '../users/types'
2
2
 
3
+ export type PersonalAccessTokenStatus = 'active' | 'revoked' | 'expired'
4
+
5
+ /** Accepted lifetimes of a token (`No expiration` keeps `accessTokenExpiresAt` null). */
6
+ export type PersonalAccessTokenExpiresIn = '30d' | '90d' | '180d' | '1y' | 'No expiration'
7
+
8
+ /**
9
+ * A personal access token of the authenticated user.
10
+ *
11
+ * `accessToken` comes in full only in the `create` answer; every other route masks it
12
+ * (`abcde***vwxyz`). `status` and `user` (only `{ name }`) come on `getMany`/`getById`.
13
+ * `deleteById` revokes the token (soft delete): it keeps being listed with `status: 'revoked'`.
14
+ */
3
15
  export type PersonalAccessToken = {
4
16
  id: string
5
17
  name: string
18
+ /** `*` when created without a scope. */
6
19
  scope: string | null
7
20
  accessToken: string
8
21
  accessTokenExpiresAt: string | null
9
22
  userId: string
23
+ clientId: string | null
24
+ impersonate?: boolean | null
10
25
  createdAt: string
11
26
  updatedAt: string
12
27
  deletedAt: string | null
28
+ /** Only on `getMany`/`getById` answers. */
29
+ status?: PersonalAccessTokenStatus
13
30
  // relationships
14
- user?: User
31
+ /** Only on `getMany`/`getById` answers, with just the name. */
32
+ user?: Pick<User, 'name'>
15
33
  }
16
34
 
17
35
  export type PersonalAccessTokenRelationships = 'user'
18
36
 
19
37
  export type CreatePersonalAccessTokenPayload = {
38
+ /** Up to 255 characters. */
20
39
  name: string
40
+ /** Up to 255 characters (`*` by default). */
21
41
  scope?: string
22
- accessTokenExpiresAt?: string
42
+ expiresIn?: PersonalAccessTokenExpiresIn
23
43
  }
24
44
 
25
45
  export type UpdatePersonalAccessTokenPayload = {
26
- name?: string
27
- scope?: string | null
46
+ /** Required by the backend on every update (400 `required` otherwise). */
47
+ name: string
48
+ scope?: string
28
49
  }
@@ -0,0 +1,273 @@
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 { DashboardApi } from './dashboard/DashboardApi'
9
+ import { NowApi } from './now/NowApi'
10
+ import { PermissionsApi } from './permissions/PermissionsApi'
11
+ import { ServiceAccessManagementApi } from './serviceAccessManagement/ServiceAccessManagementApi'
12
+ import { StickerUsersApi } from './stickerUsers/StickerUsersApi'
13
+ import { TermsApi } from './terms/TermsApi'
14
+
15
+ /**
16
+ * Every method that sends a list/get query must serialize it in the client's `queryFormat` and
17
+ * honor `withQueryFormat`. A method listed here that ignored the format would fail below.
18
+ */
19
+
20
+ function createMockClient(queryFormat?: QueryFormat): ApiClient {
21
+ return {
22
+ ...(queryFormat && { queryFormat }),
23
+ setAccessToken: vi.fn().mockReturnThis(),
24
+ request: vi.fn(),
25
+ get: vi.fn().mockResolvedValue([]),
26
+ post: vi.fn(),
27
+ put: vi.fn(),
28
+ patch: vi.fn(),
29
+ delete: vi.fn(),
30
+ }
31
+ }
32
+
33
+ const listQuery = { where: { name: 'x' }, page: 2, perPage: 5 }
34
+ const getQuery = { attributes: ['id'] }
35
+
36
+ type Case = {
37
+ name: string
38
+ path: string
39
+ query: object
40
+ /** Calls the method on the API built from `client`, optionally through `withQueryFormat`. */
41
+ call: (client: ApiClient, override?: QueryFormat) => Promise<unknown>
42
+ }
43
+
44
+ const cases: Case[] = [
45
+ {
46
+ name: 'ContactsApi.getMany (BaseCrudApi)',
47
+ path: '/contacts',
48
+ query: listQuery,
49
+ call: (client, f) => {
50
+ const api = new ContactsApi(client)
51
+ return (f ? api.withQueryFormat(f) : api).getMany(listQuery)
52
+ },
53
+ },
54
+ {
55
+ name: 'ContactsApi.getById (BaseCrudApi)',
56
+ path: '/contacts/c1',
57
+ query: getQuery,
58
+ call: (client, f) => {
59
+ const api = new ContactsApi(client)
60
+ return (f ? api.withQueryFormat(f) : api).getById('c1', { attributes: ['id'] })
61
+ },
62
+ },
63
+ {
64
+ name: 'ContactsApi.count',
65
+ path: '/contacts/count',
66
+ query: listQuery,
67
+ call: (client, f) => {
68
+ const api = new ContactsApi(client)
69
+ return (f ? api.withQueryFormat(f) : api).count(listQuery)
70
+ },
71
+ },
72
+ {
73
+ name: 'ContactsApi.forward',
74
+ path: '/contacts/forward',
75
+ query: listQuery,
76
+ call: (client, f) => {
77
+ const api = new ContactsApi(client)
78
+ return (f ? api.withQueryFormat(f) : api).forward(listQuery)
79
+ },
80
+ },
81
+ {
82
+ name: 'AuthHistoryApi.getMany',
83
+ path: '/auth-history',
84
+ query: { page: 2, perPage: 5 },
85
+ call: (client, f) => {
86
+ const api = new AuthHistoryApi(client)
87
+ return (f ? api.withQueryFormat(f) : api).getMany({ page: 2, perPage: 5 })
88
+ },
89
+ },
90
+ {
91
+ name: 'DashboardApi.getGeneral',
92
+ path: '/dashboard/general',
93
+ query: { status: 'closed', departmentId: ['d1'] },
94
+ call: (client, f) => {
95
+ const api = new DashboardApi(client)
96
+ return (f ? api.withQueryFormat(f) : api).getGeneral({
97
+ status: 'closed',
98
+ departmentId: ['d1'],
99
+ })
100
+ },
101
+ },
102
+ {
103
+ name: 'NowApi.getResume',
104
+ path: '/now/resume',
105
+ query: { departmentId: ['d1'], userId: 'u1' },
106
+ call: (client, f) => {
107
+ const api = new NowApi(client)
108
+ return (f ? api.withQueryFormat(f) : api).getResume({ departmentId: ['d1'], userId: 'u1' })
109
+ },
110
+ },
111
+ {
112
+ name: 'NowApi.getAttendanceResume',
113
+ path: '/now/attendance-resume',
114
+ query: { departmentId: ['d1'], userId: 'u1' },
115
+ call: (client, f) => {
116
+ const api = new NowApi(client)
117
+ return (f ? api.withQueryFormat(f) : api).getAttendanceResume({
118
+ departmentId: ['d1'],
119
+ userId: 'u1',
120
+ })
121
+ },
122
+ },
123
+ {
124
+ name: 'NowApi.getDepartmentsResume',
125
+ path: '/now/departments-resume',
126
+ query: { departmentId: ['d1'], userId: 'u1' },
127
+ call: (client, f) => {
128
+ const api = new NowApi(client)
129
+ return (f ? api.withQueryFormat(f) : api).getDepartmentsResume({
130
+ departmentId: ['d1'],
131
+ userId: 'u1',
132
+ })
133
+ },
134
+ },
135
+ {
136
+ name: 'PermissionsApi.getMany',
137
+ path: '/permissions',
138
+ query: listQuery,
139
+ call: (client, f) => {
140
+ const api = new PermissionsApi(client)
141
+ return (f ? api.withQueryFormat(f) : api).getMany(listQuery)
142
+ },
143
+ },
144
+ {
145
+ name: 'PermissionsApi.getById',
146
+ path: '/permissions/p1',
147
+ query: getQuery,
148
+ call: (client, f) => {
149
+ const api = new PermissionsApi(client)
150
+ return (f ? api.withQueryFormat(f) : api).getById('p1', { attributes: ['id'] })
151
+ },
152
+ },
153
+ {
154
+ name: 'AuthHistoryApi.getById',
155
+ path: '/auth-history/a1',
156
+ query: getQuery,
157
+ call: (client, f) => {
158
+ const api = new AuthHistoryApi(client)
159
+ return (f ? api.withQueryFormat(f) : api).getById('a1', { attributes: ['id'] })
160
+ },
161
+ },
162
+ {
163
+ name: 'ServiceAccessManagementApi.getMany',
164
+ path: '/service-access-management',
165
+ query: { page: 2, perPage: 5 },
166
+ call: (client, f) => {
167
+ const api = new ServiceAccessManagementApi(client)
168
+ return (f ? api.withQueryFormat(f) : api).getMany({ page: 2, perPage: 5 })
169
+ },
170
+ },
171
+ {
172
+ name: 'StickerUsersApi.getMany',
173
+ path: '/sticker-users',
174
+ query: { page: 2, perPage: 5 },
175
+ call: (client, f) => {
176
+ const api = new StickerUsersApi(client)
177
+ return (f ? api.withQueryFormat(f) : api).getMany({ page: 2, perPage: 5 })
178
+ },
179
+ },
180
+ {
181
+ name: 'TermsApi.getById',
182
+ path: '/terms/t1',
183
+ query: getQuery,
184
+ call: (client, f) => {
185
+ const api = new TermsApi(client)
186
+ return (f ? api.withQueryFormat(f) : api).getById('t1', { attributes: ['id'] })
187
+ },
188
+ },
189
+ ]
190
+
191
+ const jsonUrl = (path: string, query: object) =>
192
+ `${path}?query=${encodeURIComponent(JSON.stringify(query))}`
193
+ const qsUrl = (path: string, query: object) => `${path}?${qs.stringify(query)}`
194
+ const calledUrl = (client: ApiClient) => vi.mocked(client.get).mock.calls.at(0)?.at(0)
195
+
196
+ describe.each(cases)('$name', ({ path, query, call }) => {
197
+ it("sends JSON with the default client format ('json')", async () => {
198
+ const client = createMockClient()
199
+ await call(client)
200
+ expect(calledUrl(client)).toBe(jsonUrl(path, query))
201
+ })
202
+
203
+ it("sends bracket notation with a 'qs' client", async () => {
204
+ const client = createMockClient('qs')
205
+ await call(client)
206
+ expect(calledUrl(client)).toBe(qsUrl(path, query))
207
+ })
208
+
209
+ it("withQueryFormat('qs') overrides a 'json' client", async () => {
210
+ const client = createMockClient()
211
+ await call(client, 'qs')
212
+ expect(calledUrl(client)).toBe(qsUrl(path, query))
213
+ })
214
+
215
+ it("withQueryFormat('json') overrides a 'qs' client", async () => {
216
+ const client = createMockClient('qs')
217
+ await call(client, 'json')
218
+ expect(calledUrl(client)).toBe(jsonUrl(path, query))
219
+ })
220
+ })
221
+
222
+ describe('query format through BaseApiClient', () => {
223
+ afterEach(() => {
224
+ vi.restoreAllMocks()
225
+ })
226
+
227
+ const mockFetch = () =>
228
+ vi
229
+ .spyOn(globalThis, 'fetch')
230
+ .mockImplementation(
231
+ async () =>
232
+ new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } }),
233
+ )
234
+
235
+ it('builds the full ?query=<JSON> URL by default, keeping null typed', async () => {
236
+ const fetchSpy = mockFetch()
237
+ const contacts = new ContactsApi(new BaseApiClient('https://api.example.com/'))
238
+
239
+ await contacts.getMany({ where: { name: null }, paginate: false })
240
+
241
+ const query = '{"where":{"name":null},"paginate":false}'
242
+ expect(fetchSpy.mock.calls[0]?.[0]).toBe(
243
+ `https://api.example.com/contacts?query=${encodeURIComponent(query)}`,
244
+ )
245
+ })
246
+
247
+ it("builds the bracket URL with queryFormat 'qs'", async () => {
248
+ const fetchSpy = mockFetch()
249
+ const client = new BaseApiClient('https://api.example.com', undefined, { queryFormat: 'qs' })
250
+
251
+ await new ContactsApi(client).getMany({ where: { name: 'x' }, paginate: false })
252
+
253
+ expect(decodeURIComponent(fetchSpy.mock.calls[0]?.[0] as string)).toBe(
254
+ 'https://api.example.com/contacts?where[name]=x&paginate=false',
255
+ )
256
+ })
257
+
258
+ it("setAccessToken on a copy's client only affects the copy", async () => {
259
+ const fetchSpy = mockFetch()
260
+ const client = new BaseApiClient('https://api.example.com', 'original')
261
+ const contacts = new ContactsApi(client)
262
+ const copy = contacts.withQueryFormat('qs')
263
+
264
+ copy.client.setAccessToken('copy-only')
265
+ await contacts.getMany({ paginate: false })
266
+ await copy.getMany({ paginate: false })
267
+
268
+ const authorization = (call: number) =>
269
+ (fetchSpy.mock.calls[call]?.[1]?.headers as Record<string, string>).Authorization
270
+ expect(authorization(0)).toBe('Bearer original')
271
+ expect(authorization(1)).toBe('Bearer copy-only')
272
+ })
273
+ })
@@ -1,8 +1,13 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
2
  import { BaseCrudApi } from '../../core/BaseCrudApi'
3
- import type { Role, CreateRolePayload, UpdateRolePayload } from './types'
3
+ import type { Role, CreateRolePayload, UpdateRolePayload, RolesListQuery } from './types'
4
4
 
5
- export class RolesApi extends BaseCrudApi<Role, CreateRolePayload, UpdateRolePayload> {
5
+ export class RolesApi extends BaseCrudApi<
6
+ Role,
7
+ CreateRolePayload,
8
+ UpdateRolePayload,
9
+ RolesListQuery
10
+ > {
6
11
  constructor(client: ApiClient) {
7
12
  super(client, '/roles')
8
13
  }