@ikatec/digisac-api-sdk 4.2.0 → 4.4.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 (66) hide show
  1. package/dist/apis/contacts/ContactsApi.cjs +70 -0
  2. package/dist/apis/contacts/ContactsApi.cjs.map +1 -1
  3. package/dist/apis/contacts/ContactsApi.d.ts +51 -1
  4. package/dist/apis/contacts/ContactsApi.d.ts.map +1 -1
  5. package/dist/apis/contacts/ContactsApi.mjs +70 -0
  6. package/dist/apis/contacts/ContactsApi.mjs.map +1 -1
  7. package/dist/apis/contacts/types.d.ts +141 -1
  8. package/dist/apis/contacts/types.d.ts.map +1 -1
  9. package/dist/apis/dashboard/DashboardApi.cjs +36 -1
  10. package/dist/apis/dashboard/DashboardApi.cjs.map +1 -1
  11. package/dist/apis/dashboard/DashboardApi.d.ts +55 -2
  12. package/dist/apis/dashboard/DashboardApi.d.ts.map +1 -1
  13. package/dist/apis/dashboard/DashboardApi.mjs +36 -1
  14. package/dist/apis/dashboard/DashboardApi.mjs.map +1 -1
  15. package/dist/apis/dashboard/types.d.ts +71 -0
  16. package/dist/apis/dashboard/types.d.ts.map +1 -1
  17. package/dist/apis/services/ServicesApi.cjs +8 -0
  18. package/dist/apis/services/ServicesApi.cjs.map +1 -1
  19. package/dist/apis/services/ServicesApi.d.ts +7 -1
  20. package/dist/apis/services/ServicesApi.d.ts.map +1 -1
  21. package/dist/apis/services/ServicesApi.mjs +8 -0
  22. package/dist/apis/services/ServicesApi.mjs.map +1 -1
  23. package/dist/apis/services/types.d.ts +7 -0
  24. package/dist/apis/services/types.d.ts.map +1 -1
  25. package/dist/apis/tags/TagsApi.cjs +23 -0
  26. package/dist/apis/tags/TagsApi.cjs.map +1 -1
  27. package/dist/apis/tags/TagsApi.d.ts +19 -1
  28. package/dist/apis/tags/TagsApi.d.ts.map +1 -1
  29. package/dist/apis/tags/TagsApi.mjs +23 -0
  30. package/dist/apis/tags/TagsApi.mjs.map +1 -1
  31. package/dist/apis/tags/types.d.ts +8 -0
  32. package/dist/apis/tags/types.d.ts.map +1 -1
  33. package/dist/apis/users/UsersApi.cjs +77 -0
  34. package/dist/apis/users/UsersApi.cjs.map +1 -1
  35. package/dist/apis/users/UsersApi.d.ts +59 -1
  36. package/dist/apis/users/UsersApi.d.ts.map +1 -1
  37. package/dist/apis/users/UsersApi.mjs +77 -0
  38. package/dist/apis/users/UsersApi.mjs.map +1 -1
  39. package/dist/apis/users/types.d.ts +102 -0
  40. package/dist/apis/users/types.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/src/apis/acceptanceTerms/AcceptanceTermsApi.test.ts +1 -1
  43. package/src/apis/authHistory/AuthHistoryApi.test.ts +1 -1
  44. package/src/apis/campaigns/CampaignsApi.test.ts +1 -1
  45. package/src/apis/contacts/ContactsApi.test.ts +182 -5
  46. package/src/apis/contacts/ContactsApi.ts +128 -0
  47. package/src/apis/contacts/types.ts +154 -1
  48. package/src/apis/dashboard/DashboardApi.test-d.ts +49 -0
  49. package/src/apis/dashboard/DashboardApi.test.ts +197 -9
  50. package/src/apis/dashboard/DashboardApi.ts +130 -2
  51. package/src/apis/dashboard/types.ts +78 -0
  52. package/src/apis/messages/MessagesApi.test.ts +3 -3
  53. package/src/apis/payloads.test-d.ts +26 -0
  54. package/src/apis/queryFormat.test.ts +91 -3
  55. package/src/apis/serviceAccessManagement/ServiceAccessManagementApi.test.ts +2 -2
  56. package/src/apis/services/ServicesApi.test.ts +49 -0
  57. package/src/apis/services/ServicesApi.ts +10 -1
  58. package/src/apis/services/types.ts +9 -0
  59. package/src/apis/stickerUsers/StickerUsersApi.test.ts +1 -1
  60. package/src/apis/tags/TagsApi.test.ts +79 -0
  61. package/src/apis/tags/TagsApi.ts +57 -1
  62. package/src/apis/tags/types.ts +9 -0
  63. package/src/apis/terms/TermsApi.test.ts +1 -1
  64. package/src/apis/users/UsersApi.test.ts +228 -0
  65. package/src/apis/users/UsersApi.ts +152 -1
  66. package/src/apis/users/types.ts +121 -0
@@ -0,0 +1,228 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { UsersApi } from './UsersApi'
3
+ import type { ApiClient } from '../../core/ApiClient'
4
+ import type {
5
+ BulkUsersStatusPreview,
6
+ BulkUsersStatusResult,
7
+ ImportUsersResult,
8
+ User,
9
+ UserImportFile,
10
+ ValidateUserImportResult,
11
+ } from './types'
12
+
13
+ // ── Mock client factory ──────────────────────────────────────────────
14
+
15
+ function createMockClient(): ApiClient {
16
+ return {
17
+ setAccessToken: vi.fn().mockReturnThis(),
18
+ request: vi.fn(),
19
+ get: vi.fn(),
20
+ post: vi.fn(),
21
+ put: vi.fn(),
22
+ patch: vi.fn(),
23
+ delete: vi.fn(),
24
+ }
25
+ }
26
+
27
+ // ── Fixtures ─────────────────────────────────────────────────────────
28
+
29
+ const user: User = {
30
+ id: 'usr-1',
31
+ name: 'Alice',
32
+ email: 'alice@example.com',
33
+ phoneNumber: null,
34
+ branch: null,
35
+ isSuperAdmin: false,
36
+ isClientUser: false,
37
+ active: true,
38
+ language: 'pt-BR',
39
+ isFirstLogin: false,
40
+ status: 'offline',
41
+ clientsStatus: { web: 'offline', app: 'offline' },
42
+ offlineAt: null,
43
+ isActiveInternalChat: false,
44
+ passwordExpiresAt: null,
45
+ otpAuthActive: false,
46
+ preferences: {},
47
+ archivedAt: null,
48
+ accountId: 'acc-1',
49
+ timetableId: null,
50
+ createdAt: '2024-01-01T00:00:00Z',
51
+ updatedAt: '2024-01-01T00:00:00Z',
52
+ deletedAt: null,
53
+ }
54
+
55
+ const file: UserImportFile = { id: 'f1', name: 'users.csv', mimetype: 'text/csv' }
56
+ const headers = { 'X-Custom': 'value' }
57
+
58
+ // ── Tests ────────────────────────────────────────────────────────────
59
+
60
+ describe('UsersApi', () => {
61
+ let client: ReturnType<typeof createMockClient>
62
+ let api: UsersApi
63
+
64
+ beforeEach(() => {
65
+ client = createMockClient()
66
+ api = new UsersApi(client)
67
+ })
68
+
69
+ describe('archive (ArchivableCrudApi)', () => {
70
+ it('keeps the plain PUT { archivedAt } used by the cleanup of the test suites', async () => {
71
+ await api.archive('usr-1')
72
+ const [url, body] = vi.mocked(client.put).mock.calls[0] ?? []
73
+ expect(url).toBe('/users/usr-1')
74
+ expect(body).toEqual({ archivedAt: expect.any(String) })
75
+ })
76
+ })
77
+
78
+ describe('archiveWithValidation', () => {
79
+ it('calls client.post with /users/:id/archive and { archive: true }', async () => {
80
+ const archived = { ...user, archivedAt: '2026-01-01T00:00:00Z' }
81
+ vi.mocked(client.post).mockResolvedValue(archived)
82
+ const result = await api.archiveWithValidation('usr-1', { archive: true })
83
+ expect(client.post).toHaveBeenCalledWith('/users/usr-1/archive', { archive: true }, undefined)
84
+ expect(result).toEqual(archived)
85
+ })
86
+
87
+ it('restores with { archive: false } and forwards custom headers', async () => {
88
+ await api.archiveWithValidation('usr-1', { archive: false }, headers)
89
+ expect(client.post).toHaveBeenCalledWith('/users/usr-1/archive', { archive: false }, headers)
90
+ })
91
+ })
92
+
93
+ describe('previewBulkStatus', () => {
94
+ it('calls client.post with /users/bulk-status/preview and body', async () => {
95
+ const preview: BulkUsersStatusPreview = {
96
+ scenario: 'allEligible',
97
+ totalSelected: 1,
98
+ noop: 0,
99
+ eligible: 1,
100
+ blocked: 0,
101
+ blockedSelf: false,
102
+ planSlotsAvailable: 10,
103
+ }
104
+ vi.mocked(client.post).mockResolvedValue(preview)
105
+ const body = { ids: ['usr-1'], archive: false }
106
+ const result = await api.previewBulkStatus(body)
107
+ expect(client.post).toHaveBeenCalledWith('/users/bulk-status/preview', body, undefined)
108
+ expect(result).toEqual(preview)
109
+ })
110
+ })
111
+
112
+ describe('bulkStatus', () => {
113
+ it('calls client.post with /users/bulk-status and body', async () => {
114
+ const response: BulkUsersStatusResult = {
115
+ changed: 1,
116
+ noop: 0,
117
+ blocked: 0,
118
+ failed: 0,
119
+ changedIds: ['usr-1'],
120
+ missingIds: [],
121
+ }
122
+ vi.mocked(client.post).mockResolvedValue(response)
123
+ const body = { ids: ['usr-1'], archive: true }
124
+ const result = await api.bulkStatus(body, headers)
125
+ expect(client.post).toHaveBeenCalledWith('/users/bulk-status', body, headers)
126
+ expect(result).toEqual(response)
127
+ })
128
+ })
129
+
130
+ describe('exportCsv', () => {
131
+ it('calls client.post with /users/export and resolves to the CSV text', async () => {
132
+ const csv = 'Nome,E-mail\nAlice,alice@example.com\n'
133
+ vi.mocked(client.post).mockResolvedValue(csv)
134
+ const result = await api.exportCsv({ filter: 'active' })
135
+ expect(client.post).toHaveBeenCalledWith('/users/export', { filter: 'active' }, undefined)
136
+ expect(result).toBe(csv)
137
+ })
138
+ })
139
+
140
+ describe('validateImport', () => {
141
+ it('calls client.post with /users/import/validate and the file', async () => {
142
+ const response: ValidateUserImportResult = {
143
+ licenses: { requestedRows: 1, available: 5, exceeded: false },
144
+ columns: {
145
+ required: [{ name: 'Nome', found: true }],
146
+ missingRequired: [],
147
+ optionalFound: [],
148
+ },
149
+ rows: { total: 1, valid: 1, invalid: 0, hasInvalid: false },
150
+ rowsWithOptionalColumns: { total: 1, valid: 1, invalid: 0, hasInvalid: false },
151
+ }
152
+ vi.mocked(client.post).mockResolvedValue(response)
153
+ const result = await api.validateImport({ file })
154
+ expect(client.post).toHaveBeenCalledWith('/users/import/validate', { file }, undefined)
155
+ expect(result).toEqual(response)
156
+ })
157
+ })
158
+
159
+ describe('startImport', () => {
160
+ it('calls client.post with /users/import and body', async () => {
161
+ const response: ImportUsersResult = { importId: 'imp-1' }
162
+ vi.mocked(client.post).mockResolvedValue(response)
163
+ const body = { file, useOptionalColumns: true }
164
+ const result = await api.startImport(body, headers)
165
+ expect(client.post).toHaveBeenCalledWith('/users/import', body, headers)
166
+ expect(result).toEqual(response)
167
+ })
168
+ })
169
+
170
+ describe('disableOtpAuth', () => {
171
+ it('calls client.post with /users/:id/disable-otp-auth and no body', async () => {
172
+ vi.mocked(client.post).mockResolvedValue(user)
173
+ const result = await api.disableOtpAuth('usr-1')
174
+ expect(client.post).toHaveBeenCalledWith(
175
+ '/users/usr-1/disable-otp-auth',
176
+ undefined,
177
+ undefined,
178
+ )
179
+ expect(result).toEqual(user)
180
+ })
181
+ })
182
+
183
+ describe('enableAccess', () => {
184
+ it('calls client.post with /users/:id/enable-access and no body', async () => {
185
+ const response = { success: true, message: 'User access enabled successfully' }
186
+ vi.mocked(client.post).mockResolvedValue(response)
187
+ const result = await api.enableAccess('usr-1', headers)
188
+ expect(client.post).toHaveBeenCalledWith('/users/usr-1/enable-access', undefined, headers)
189
+ expect(result).toEqual(response)
190
+ })
191
+ })
192
+
193
+ describe('getPinnedContacts', () => {
194
+ it('calls client.get with /users/pin', async () => {
195
+ vi.mocked(client.get).mockResolvedValue([])
196
+ const result = await api.getPinnedContacts()
197
+ expect(client.get).toHaveBeenCalledWith('/users/pin', undefined)
198
+ expect(result).toEqual([])
199
+ })
200
+ })
201
+
202
+ describe('togglePinContact', () => {
203
+ it('calls client.patch with /users/pin and resolves to the link rows when pinning', async () => {
204
+ const links = [
205
+ { userId: 'usr-1', contactId: 'c1', createdAt: '2026-01-01', updatedAt: '2026-01-01' },
206
+ ]
207
+ vi.mocked(client.patch).mockResolvedValue(links)
208
+ const result = await api.togglePinContact({ contactId: 'c1' })
209
+ expect(client.patch).toHaveBeenCalledWith('/users/pin', { contactId: 'c1' }, undefined)
210
+ expect(result).toEqual(links)
211
+ })
212
+
213
+ it('resolves to the number of removed links when unpinning', async () => {
214
+ vi.mocked(client.patch).mockResolvedValue(1)
215
+ await expect(api.togglePinContact({ contactId: 'c1' }, headers)).resolves.toBe(1)
216
+ expect(client.patch).toHaveBeenCalledWith('/users/pin', { contactId: 'c1' }, headers)
217
+ })
218
+ })
219
+
220
+ describe('error propagation', () => {
221
+ it('propagates errors from archiveWithValidation', async () => {
222
+ vi.mocked(client.post).mockRejectedValue(new Error('Bad request'))
223
+ await expect(api.archiveWithValidation('usr-1', { archive: true })).rejects.toThrow(
224
+ 'Bad request',
225
+ )
226
+ })
227
+ })
228
+ })
@@ -1,9 +1,160 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
2
  import { ArchivableCrudApi } from '../../core/ArchivableCrudApi'
3
- import type { User, CreateUserPayload, UpdateUserPayload } from './types'
3
+ import type { Contact } from '../contacts/types'
4
+ import type {
5
+ ArchiveUserPayload,
6
+ BulkUsersStatusPayload,
7
+ BulkUsersStatusPreview,
8
+ BulkUsersStatusResult,
9
+ CreateUserPayload,
10
+ EnableUserAccessResult,
11
+ ExportUsersPayload,
12
+ ImportUsersPayload,
13
+ ImportUsersResult,
14
+ TogglePinContactPayload,
15
+ TogglePinContactResult,
16
+ UpdateUserPayload,
17
+ User,
18
+ ValidateUserImportPayload,
19
+ ValidateUserImportResult,
20
+ } from './types'
4
21
 
5
22
  export class UsersApi extends ArchivableCrudApi<User, CreateUserPayload, UpdateUserPayload> {
6
23
  constructor(client: ApiClient) {
7
24
  super(client, '/users')
8
25
  }
26
+
27
+ /**
28
+ * Archives or restores a user through the dedicated route used by the UI, which also
29
+ * sets the user offline and ends its sessions. The route enforces the business rules that
30
+ * {@link archive}/{@link unarchive} (a plain `PUT { archivedAt }`) skip:
31
+ * - archiving refuses (403) a user referenced by a bot or with open tickets;
32
+ * - restoring refuses (402) when the plan has no free user slot.
33
+ * @permissions users.view OR users.archive
34
+ */
35
+ archiveWithValidation(
36
+ id: string,
37
+ body: ArchiveUserPayload,
38
+ headers?: Record<string, string>,
39
+ ): Promise<User> {
40
+ return this.client.post<User, ArchiveUserPayload>(
41
+ `${this.basePath}/${id}/archive`,
42
+ body,
43
+ headers,
44
+ )
45
+ }
46
+
47
+ /**
48
+ * Previews a bulk archive/unarchive without changing anything.
49
+ * @permissions users.archive
50
+ */
51
+ previewBulkStatus(
52
+ body: BulkUsersStatusPayload,
53
+ headers?: Record<string, string>,
54
+ ): Promise<BulkUsersStatusPreview> {
55
+ return this.client.post<BulkUsersStatusPreview, BulkUsersStatusPayload>(
56
+ `${this.basePath}/bulk-status/preview`,
57
+ body,
58
+ headers,
59
+ )
60
+ }
61
+
62
+ /**
63
+ * Archives or unarchives up to 100 users at once.
64
+ * @permissions users.archive
65
+ */
66
+ bulkStatus(
67
+ body: BulkUsersStatusPayload,
68
+ headers?: Record<string, string>,
69
+ ): Promise<BulkUsersStatusResult> {
70
+ return this.client.post<BulkUsersStatusResult, BulkUsersStatusPayload>(
71
+ `${this.basePath}/bulk-status`,
72
+ body,
73
+ headers,
74
+ )
75
+ }
76
+
77
+ /**
78
+ * Exports the users of the account as CSV. Resolves to the CSV text.
79
+ * @permissions users.export
80
+ */
81
+ exportCsv(body: ExportUsersPayload, headers?: Record<string, string>): Promise<string> {
82
+ return this.client.post<string, ExportUsersPayload>(`${this.basePath}/export`, body, headers)
83
+ }
84
+
85
+ /**
86
+ * Validates a CSV file previously uploaded through `FilesApi` before importing it.
87
+ * @permissions users.create
88
+ */
89
+ validateImport(
90
+ body: ValidateUserImportPayload,
91
+ headers?: Record<string, string>,
92
+ ): Promise<ValidateUserImportResult> {
93
+ return this.client.post<ValidateUserImportResult, ValidateUserImportPayload>(
94
+ `${this.basePath}/import/validate`,
95
+ body,
96
+ headers,
97
+ )
98
+ }
99
+
100
+ /**
101
+ * Starts the import of a CSV file previously uploaded through `FilesApi`. The import runs
102
+ * in the background (the route answers `202 Accepted`).
103
+ * @permissions users.create
104
+ */
105
+ startImport(
106
+ body: ImportUsersPayload,
107
+ headers?: Record<string, string>,
108
+ ): Promise<ImportUsersResult> {
109
+ return this.client.post<ImportUsersResult, ImportUsersPayload>(
110
+ `${this.basePath}/import`,
111
+ body,
112
+ headers,
113
+ )
114
+ }
115
+
116
+ /**
117
+ * Turns off the two-factor authentication (OTP) of a user.
118
+ * @permissions users.update AND myAccount.update
119
+ */
120
+ disableOtpAuth(id: string, headers?: Record<string, string>): Promise<User> {
121
+ return this.client.post<User>(`${this.basePath}/${id}/disable-otp-auth`, undefined, headers)
122
+ }
123
+
124
+ /**
125
+ * Clears the login throttling of a user blocked by too many failed attempts.
126
+ * @permissions users.update
127
+ */
128
+ enableAccess(id: string, headers?: Record<string, string>): Promise<EnableUserAccessResult> {
129
+ return this.client.post<EnableUserAccessResult>(
130
+ `${this.basePath}/${id}/enable-access`,
131
+ undefined,
132
+ headers,
133
+ )
134
+ }
135
+
136
+ /**
137
+ * Returns the contacts pinned by the logged user.
138
+ * @permissions contacts.pin
139
+ */
140
+ getPinnedContacts(headers?: Record<string, string>): Promise<Contact[]> {
141
+ return this.client.get<Contact[]>(`${this.basePath}/pin`, headers)
142
+ }
143
+
144
+ /**
145
+ * Pins the contact for the logged user, or unpins it when already pinned. A user can pin
146
+ * up to 3 contacts. Resolves to the created link rows when pinning, or to the number of
147
+ * removed links when unpinning.
148
+ * @permissions contacts.pin
149
+ */
150
+ togglePinContact(
151
+ body: TogglePinContactPayload,
152
+ headers?: Record<string, string>,
153
+ ): Promise<TogglePinContactResult> {
154
+ return this.client.patch<TogglePinContactResult, TogglePinContactPayload>(
155
+ `${this.basePath}/pin`,
156
+ body,
157
+ headers,
158
+ )
159
+ }
9
160
  }
@@ -69,3 +69,124 @@ export type UpdateUserPayload = {
69
69
  language?: string
70
70
  archivedAt?: string | null
71
71
  }
72
+
73
+ /** Body for `POST /users/:id/archive`. */
74
+ export type ArchiveUserPayload = {
75
+ /** `true` archives the user; `false` restores it. */
76
+ archive: boolean
77
+ }
78
+
79
+ /** Body for `POST /users/bulk-status` and `POST /users/bulk-status/preview` (1 to 100 IDs). */
80
+ export type BulkUsersStatusPayload = {
81
+ ids: string[]
82
+ archive: boolean
83
+ }
84
+
85
+ export type BulkUsersStatusScenario =
86
+ | 'emptySelection'
87
+ | 'noneEligible'
88
+ | 'allEligible'
89
+ | 'partiallyBlocked'
90
+ | 'onlySelfBlocked'
91
+ | 'planLimitExceeded'
92
+
93
+ /** Read-only preview of a bulk status change: nothing is written. */
94
+ export type BulkUsersStatusPreview = {
95
+ scenario: BulkUsersStatusScenario
96
+ totalSelected: number
97
+ /** Users already in the target status. */
98
+ noop: number
99
+ eligible: number
100
+ /** Users that cannot change (e.g. referenced by a bot, or the acting user). */
101
+ blocked: number
102
+ blockedSelf: boolean
103
+ /** Free plan slots when unarchiving; `null` when archiving. */
104
+ planSlotsAvailable: number | null
105
+ }
106
+
107
+ export type BulkUsersStatusResult = {
108
+ changed: number
109
+ noop: number
110
+ blocked: number
111
+ failed: number
112
+ changedIds: string[]
113
+ /** IDs of the batch that no longer exist in the account. */
114
+ missingIds: string[]
115
+ }
116
+
117
+ /** Body for `POST /users/export`. */
118
+ export type ExportUsersPayload = {
119
+ filter: 'all' | 'active' | 'archived'
120
+ }
121
+
122
+ /** A file previously uploaded through `FilesApi`. */
123
+ export type UserImportFile = {
124
+ id: string
125
+ name: string
126
+ mimetype: string
127
+ url?: string
128
+ urlToUpload?: string
129
+ }
130
+
131
+ /** Body for `POST /users/import/validate`. */
132
+ export type ValidateUserImportPayload = {
133
+ file: UserImportFile
134
+ }
135
+
136
+ export type UserImportRowsSummary = {
137
+ total: number
138
+ valid: number
139
+ invalid: number
140
+ hasInvalid: boolean
141
+ }
142
+
143
+ export type ValidateUserImportResult = {
144
+ licenses: {
145
+ requestedRows: number
146
+ available: number
147
+ exceeded: boolean
148
+ }
149
+ columns: {
150
+ required: { name: string; found: boolean }[]
151
+ missingRequired: string[]
152
+ optionalFound: string[]
153
+ }
154
+ rows: UserImportRowsSummary
155
+ /** Same rows validated with the optional columns applied. */
156
+ rowsWithOptionalColumns: UserImportRowsSummary
157
+ }
158
+
159
+ /** Body for `POST /users/import`. */
160
+ export type ImportUsersPayload = {
161
+ file: UserImportFile
162
+ useOptionalColumns?: boolean
163
+ }
164
+
165
+ /** The import runs in the background (the route answers `202 Accepted`). */
166
+ export type ImportUsersResult = {
167
+ importId: string
168
+ }
169
+
170
+ export type EnableUserAccessResult = {
171
+ success: boolean
172
+ message: string
173
+ }
174
+
175
+ /** Body for `PATCH /users/pin`: toggles the pin of a contact for the logged user (max. 3). */
176
+ export type TogglePinContactPayload = {
177
+ contactId: string
178
+ }
179
+
180
+ /** Link row returned by `PATCH /users/pin` when the contact gets pinned. */
181
+ export type UserPinnedContactLink = {
182
+ userId: string
183
+ contactId: string
184
+ createdAt: string
185
+ updatedAt: string
186
+ }
187
+
188
+ /**
189
+ * Result of `PATCH /users/pin`: the created link rows when the contact gets pinned, or the
190
+ * number of removed links when it gets unpinned.
191
+ */
192
+ export type TogglePinContactResult = UserPinnedContactLink[] | number