@ikatec/digisac-api-sdk 4.1.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 (126) hide show
  1. package/README.md +7 -4
  2. package/dist/apis/authHistory/AuthHistoryApi.cjs +12 -12
  3. package/dist/apis/authHistory/AuthHistoryApi.cjs.map +1 -1
  4. package/dist/apis/authHistory/AuthHistoryApi.d.ts +12 -19
  5. package/dist/apis/authHistory/AuthHistoryApi.d.ts.map +1 -1
  6. package/dist/apis/authHistory/AuthHistoryApi.mjs +12 -12
  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/customFields/types.d.ts +21 -19
  11. package/dist/apis/customFields/types.d.ts.map +1 -1
  12. package/dist/apis/dashboard/DashboardApi.cjs +19 -0
  13. package/dist/apis/dashboard/DashboardApi.cjs.map +1 -0
  14. package/dist/apis/dashboard/DashboardApi.d.ts +12 -0
  15. package/dist/apis/dashboard/DashboardApi.d.ts.map +1 -0
  16. package/dist/apis/dashboard/DashboardApi.mjs +18 -0
  17. package/dist/apis/dashboard/DashboardApi.mjs.map +1 -0
  18. package/dist/apis/dashboard/index.cjs +4 -0
  19. package/dist/apis/dashboard/index.d.ts +3 -0
  20. package/dist/apis/dashboard/index.d.ts.map +1 -0
  21. package/dist/apis/dashboard/index.mjs +3 -0
  22. package/dist/apis/dashboard/types.cjs +0 -0
  23. package/dist/apis/dashboard/types.d.ts +53 -0
  24. package/dist/apis/dashboard/types.d.ts.map +1 -0
  25. package/dist/apis/dashboard/types.mjs +0 -0
  26. package/dist/apis/holiday/types.d.ts +34 -8
  27. package/dist/apis/holiday/types.d.ts.map +1 -1
  28. package/dist/apis/index.cjs +6 -0
  29. package/dist/apis/index.d.ts +2 -0
  30. package/dist/apis/index.d.ts.map +1 -1
  31. package/dist/apis/index.mjs +5 -1
  32. package/dist/apis/now/NowApi.cjs +37 -0
  33. package/dist/apis/now/NowApi.cjs.map +1 -0
  34. package/dist/apis/now/NowApi.d.ts +26 -0
  35. package/dist/apis/now/NowApi.d.ts.map +1 -0
  36. package/dist/apis/now/NowApi.mjs +36 -0
  37. package/dist/apis/now/NowApi.mjs.map +1 -0
  38. package/dist/apis/now/index.cjs +4 -0
  39. package/dist/apis/now/index.d.ts +3 -0
  40. package/dist/apis/now/index.d.ts.map +1 -0
  41. package/dist/apis/now/index.mjs +3 -0
  42. package/dist/apis/now/types.cjs +0 -0
  43. package/dist/apis/now/types.d.ts +45 -0
  44. package/dist/apis/now/types.d.ts.map +1 -0
  45. package/dist/apis/now/types.mjs +0 -0
  46. package/dist/apis/people/types.d.ts +7 -2
  47. package/dist/apis/people/types.d.ts.map +1 -1
  48. package/dist/apis/permissions/PermissionsApi.cjs +6 -2
  49. package/dist/apis/permissions/PermissionsApi.cjs.map +1 -1
  50. package/dist/apis/permissions/PermissionsApi.d.ts +7 -3
  51. package/dist/apis/permissions/PermissionsApi.d.ts.map +1 -1
  52. package/dist/apis/permissions/PermissionsApi.mjs +6 -2
  53. package/dist/apis/permissions/PermissionsApi.mjs.map +1 -1
  54. package/dist/apis/permissions/types.d.ts +4 -14
  55. package/dist/apis/permissions/types.d.ts.map +1 -1
  56. package/dist/apis/personalAccessTokens/PersonalAccessTokensApi.cjs +5 -0
  57. package/dist/apis/personalAccessTokens/PersonalAccessTokensApi.cjs.map +1 -1
  58. package/dist/apis/personalAccessTokens/PersonalAccessTokensApi.d.ts +5 -0
  59. package/dist/apis/personalAccessTokens/PersonalAccessTokensApi.d.ts.map +1 -1
  60. package/dist/apis/personalAccessTokens/PersonalAccessTokensApi.mjs +5 -0
  61. package/dist/apis/personalAccessTokens/PersonalAccessTokensApi.mjs.map +1 -1
  62. package/dist/apis/personalAccessTokens/types.d.ts +23 -4
  63. package/dist/apis/personalAccessTokens/types.d.ts.map +1 -1
  64. package/dist/apis/roles/RolesApi.cjs.map +1 -1
  65. package/dist/apis/roles/RolesApi.d.ts +2 -2
  66. package/dist/apis/roles/RolesApi.d.ts.map +1 -1
  67. package/dist/apis/roles/RolesApi.mjs.map +1 -1
  68. package/dist/apis/roles/types.d.ts +27 -4
  69. package/dist/apis/roles/types.d.ts.map +1 -1
  70. package/dist/apis/terms/TermsApi.cjs +4 -1
  71. package/dist/apis/terms/TermsApi.cjs.map +1 -1
  72. package/dist/apis/terms/TermsApi.d.ts +4 -1
  73. package/dist/apis/terms/TermsApi.d.ts.map +1 -1
  74. package/dist/apis/terms/TermsApi.mjs +4 -1
  75. package/dist/apis/terms/TermsApi.mjs.map +1 -1
  76. package/dist/apis/timetable/types.d.ts +12 -7
  77. package/dist/apis/timetable/types.d.ts.map +1 -1
  78. package/dist/core/BaseCrudApi.cjs +3 -30
  79. package/dist/core/BaseCrudApi.cjs.map +1 -1
  80. package/dist/core/BaseCrudApi.d.ts +4 -24
  81. package/dist/core/BaseCrudApi.d.ts.map +1 -1
  82. package/dist/core/BaseCrudApi.mjs +3 -30
  83. package/dist/core/BaseCrudApi.mjs.map +1 -1
  84. package/dist/core/BaseReadApi.cjs +44 -0
  85. package/dist/core/BaseReadApi.cjs.map +1 -0
  86. package/dist/core/BaseReadApi.d.ts +32 -0
  87. package/dist/core/BaseReadApi.d.ts.map +1 -0
  88. package/dist/core/BaseReadApi.mjs +43 -0
  89. package/dist/core/BaseReadApi.mjs.map +1 -0
  90. package/dist/core/index.cjs +2 -0
  91. package/dist/core/index.d.ts +1 -0
  92. package/dist/core/index.d.ts.map +1 -1
  93. package/dist/core/index.mjs +2 -1
  94. package/dist/index.cjs +2 -0
  95. package/dist/index.mjs +2 -1
  96. package/package.json +1 -1
  97. package/src/apis/authHistory/AuthHistoryApi.test.ts +10 -2
  98. package/src/apis/authHistory/AuthHistoryApi.ts +13 -39
  99. package/src/apis/authHistory/types.ts +4 -3
  100. package/src/apis/customFields/types.ts +21 -25
  101. package/src/apis/dashboard/DashboardApi.test.ts +82 -0
  102. package/src/apis/dashboard/DashboardApi.ts +18 -0
  103. package/src/apis/dashboard/index.ts +2 -0
  104. package/src/apis/dashboard/types.ts +53 -0
  105. package/src/apis/holiday/types.ts +36 -8
  106. package/src/apis/index.ts +2 -0
  107. package/src/apis/now/NowApi.test.ts +125 -0
  108. package/src/apis/now/NowApi.ts +59 -0
  109. package/src/apis/now/index.ts +2 -0
  110. package/src/apis/now/types.ts +47 -0
  111. package/src/apis/payloads.test-d.ts +39 -0
  112. package/src/apis/payloads.test.ts +81 -0
  113. package/src/apis/people/types.ts +7 -2
  114. package/src/apis/permissions/PermissionsApi.test.ts +93 -0
  115. package/src/apis/permissions/PermissionsApi.ts +7 -7
  116. package/src/apis/permissions/types.ts +4 -16
  117. package/src/apis/personalAccessTokens/PersonalAccessTokensApi.ts +5 -0
  118. package/src/apis/personalAccessTokens/types.ts +25 -4
  119. package/src/apis/queryFormat.test.ts +75 -0
  120. package/src/apis/roles/RolesApi.ts +7 -2
  121. package/src/apis/roles/types.ts +27 -4
  122. package/src/apis/terms/TermsApi.ts +4 -1
  123. package/src/apis/timetable/types.ts +13 -7
  124. package/src/core/BaseCrudApi.ts +4 -60
  125. package/src/core/BaseReadApi.ts +70 -0
  126. package/src/core/index.ts +1 -0
@@ -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
  }
@@ -5,6 +5,9 @@ import { BaseApiClient } from '../core/BaseApiClient'
5
5
  import type { QueryFormat } from '../core/queryString'
6
6
  import { AuthHistoryApi } from './authHistory/AuthHistoryApi'
7
7
  import { ContactsApi } from './contacts/ContactsApi'
8
+ import { DashboardApi } from './dashboard/DashboardApi'
9
+ import { NowApi } from './now/NowApi'
10
+ import { PermissionsApi } from './permissions/PermissionsApi'
8
11
  import { ServiceAccessManagementApi } from './serviceAccessManagement/ServiceAccessManagementApi'
9
12
  import { StickerUsersApi } from './stickerUsers/StickerUsersApi'
10
13
  import { TermsApi } from './terms/TermsApi'
@@ -84,6 +87,78 @@ const cases: Case[] = [
84
87
  return (f ? api.withQueryFormat(f) : api).getMany({ page: 2, perPage: 5 })
85
88
  },
86
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
+ },
87
162
  {
88
163
  name: 'ServiceAccessManagementApi.getMany',
89
164
  path: '/service-access-management',
@@ -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
  }
@@ -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
  }
@@ -12,7 +12,10 @@ import type { Term } from './types'
12
12
  export class TermsApi extends BaseApi implements HasGetById<Term> {
13
13
  public readonly basePath = '/terms'
14
14
 
15
- /** 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
+ */
16
19
  getById(id: string, query?: GetByIdQuery<Term>, headers?: Record<string, string>): Promise<Term> {
17
20
  const queryString = this.toQueryString(query)
18
21
  return this.client.get<Term>(`${this.basePath}/${id}${queryString}`, headers)
@@ -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,6 +1,5 @@
1
- import type { ApiClient } from './ApiClient'
2
- import { BaseApi } from './BaseApi'
3
- import type { CrudApi, GetByIdQuery, ListQuery, Paginated } from './types'
1
+ import { BaseReadApi } from './BaseReadApi'
2
+ import type { CrudApi, ListQuery } from './types'
4
3
  export type {
5
4
  WhereClause,
6
5
  IncludeItem,
@@ -15,71 +14,16 @@ export type {
15
14
  CrudApi,
16
15
  } from './types'
17
16
 
17
+ /** Generic CRUD resource: the reads of `BaseReadApi` plus `create`, `updateById` and `deleteById`. */
18
18
  export class BaseCrudApi<
19
19
  TResponse,
20
20
  TCreate,
21
21
  TUpdate = TCreate,
22
22
  TQuery extends ListQuery<TResponse> = ListQuery<TResponse>,
23
23
  >
24
- extends BaseApi
24
+ extends BaseReadApi<TResponse, TQuery>
25
25
  implements CrudApi<TResponse, TCreate, TUpdate, TQuery>
26
26
  {
27
- public readonly basePath: string
28
-
29
- constructor(client: ApiClient, basePath: string) {
30
- super(client)
31
- this.basePath = basePath.replace(/\/$/, '')
32
- }
33
-
34
- getMany(
35
- query: TQuery & { paginate: false },
36
- headers?: Record<string, string>,
37
- ): Promise<TResponse[]>
38
- getMany(
39
- query?: TQuery & { paginate?: true },
40
- headers?: Record<string, string>,
41
- ): Promise<Paginated<TResponse>>
42
- getMany(
43
- query?: TQuery,
44
- headers?: Record<string, string>,
45
- ): Promise<Paginated<TResponse> | TResponse[]>
46
- getMany(
47
- query?: TQuery,
48
- headers?: Record<string, string>,
49
- ): Promise<Paginated<TResponse> | TResponse[]> {
50
- const queryString = this.toQueryString(query)
51
- return this.client.get<Paginated<TResponse> | TResponse[]>(
52
- `${this.basePath}${queryString}`,
53
- headers,
54
- )
55
- }
56
-
57
- /**
58
- * Returns the first resource matching the query, or `null` when none match.
59
- * Convenience over `getMany` with `limit: 1`.
60
- * @permissions $resourceName.view
61
- */
62
- async getOne(query?: TQuery, headers?: Record<string, string>): Promise<TResponse | null> {
63
- const results = await this.getMany(
64
- { ...query, limit: 1, paginate: false } as TQuery & { paginate: false },
65
- headers,
66
- )
67
- // An empty response body resolves to `undefined`.
68
- return results?.[0] ?? null
69
- }
70
-
71
- /**
72
- * Returns a single resource by ID.
73
- * @permissions $resourceName.view
74
- */
75
- getById(
76
- id: string,
77
- query?: GetByIdQuery<TResponse>,
78
- headers?: Record<string, string>,
79
- ): Promise<TResponse> {
80
- return this.client.get<TResponse>(`${this.basePath}/${id}${this.toQueryString(query)}`, headers)
81
- }
82
-
83
27
  /**
84
28
  * Creates a new resource.
85
29
  * @permissions $resourceName.create
@@ -0,0 +1,70 @@
1
+ import type { ApiClient } from './ApiClient'
2
+ import { BaseApi } from './BaseApi'
3
+ import type { GetByIdQuery, HasGetById, HasGetMany, ListQuery, Paginated } from './types'
4
+
5
+ /**
6
+ * Generic read-only resource: `getMany`, `getOne` and `getById` under `basePath`. Base of
7
+ * `BaseCrudApi`, and of the resources the backend only serves for reading (e.g. permissions,
8
+ * auth history).
9
+ */
10
+ export class BaseReadApi<TResponse, TQuery extends ListQuery<TResponse> = ListQuery<TResponse>>
11
+ extends BaseApi
12
+ implements HasGetMany<TResponse, TQuery>, HasGetById<TResponse>
13
+ {
14
+ public readonly basePath: string
15
+
16
+ constructor(client: ApiClient, basePath: string) {
17
+ super(client)
18
+ this.basePath = basePath.replace(/\/$/, '')
19
+ }
20
+
21
+ getMany(
22
+ query: TQuery & { paginate: false },
23
+ headers?: Record<string, string>,
24
+ ): Promise<TResponse[]>
25
+ getMany(
26
+ query?: TQuery & { paginate?: true },
27
+ headers?: Record<string, string>,
28
+ ): Promise<Paginated<TResponse>>
29
+ getMany(
30
+ query?: TQuery,
31
+ headers?: Record<string, string>,
32
+ ): Promise<Paginated<TResponse> | TResponse[]>
33
+ getMany(
34
+ query?: TQuery,
35
+ headers?: Record<string, string>,
36
+ ): Promise<Paginated<TResponse> | TResponse[]> {
37
+ const queryString = this.toQueryString(query)
38
+ return this.client.get<Paginated<TResponse> | TResponse[]>(
39
+ `${this.basePath}${queryString}`,
40
+ headers,
41
+ )
42
+ }
43
+
44
+ /**
45
+ * Returns the first resource matching the query, or `null` when none match.
46
+ * Convenience over `getMany` with `limit: 1`.
47
+ * @permissions $resourceName.view
48
+ */
49
+ async getOne(query?: TQuery, headers?: Record<string, string>): Promise<TResponse | null> {
50
+ const results = await this.getMany(
51
+ { ...query, limit: 1, paginate: false } as TQuery & { paginate: false },
52
+ headers,
53
+ )
54
+ // An empty response body resolves to `undefined`.
55
+ return results?.[0] ?? null
56
+ }
57
+
58
+ /**
59
+ * Returns a single resource by ID. When it does not exist most routes answer 200 with an empty
60
+ * body, which resolves to `undefined`.
61
+ * @permissions $resourceName.view
62
+ */
63
+ getById(
64
+ id: string,
65
+ query?: GetByIdQuery<TResponse>,
66
+ headers?: Record<string, string>,
67
+ ): Promise<TResponse> {
68
+ return this.client.get<TResponse>(`${this.basePath}/${id}${this.toQueryString(query)}`, headers)
69
+ }
70
+ }
package/src/core/index.ts CHANGED
@@ -20,6 +20,7 @@ export {
20
20
  } from './errors'
21
21
  export type { FieldError, ErrorClassName, ApiErrorOptions } from './errors'
22
22
  export { BaseApi } from './BaseApi'
23
+ export { BaseReadApi } from './BaseReadApi'
23
24
  export { BaseCrudApi } from './BaseCrudApi'
24
25
  export { ArchivableCrudApi } from './ArchivableCrudApi'
25
26
  export type { WhereClause, IncludeItem, ListQuery, GetByIdQuery, Paginated } from './types'