@ikatec/digisac-api-sdk 4.13.0 → 4.15.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 (59) hide show
  1. package/README.md +7 -2
  2. package/dist/apis/authHistory/AuthHistoryApi.cjs +9 -2
  3. package/dist/apis/authHistory/AuthHistoryApi.cjs.map +1 -1
  4. package/dist/apis/authHistory/AuthHistoryApi.d.ts +7 -2
  5. package/dist/apis/authHistory/AuthHistoryApi.d.ts.map +1 -1
  6. package/dist/apis/authHistory/AuthHistoryApi.mjs +9 -2
  7. package/dist/apis/authHistory/AuthHistoryApi.mjs.map +1 -1
  8. package/dist/apis/authHistory/types.d.ts +6 -1
  9. package/dist/apis/authHistory/types.d.ts.map +1 -1
  10. package/dist/apis/contacts/ContactsApi.cjs +10 -0
  11. package/dist/apis/contacts/ContactsApi.cjs.map +1 -1
  12. package/dist/apis/contacts/ContactsApi.d.ts +10 -2
  13. package/dist/apis/contacts/ContactsApi.d.ts.map +1 -1
  14. package/dist/apis/contacts/ContactsApi.mjs +10 -0
  15. package/dist/apis/contacts/ContactsApi.mjs.map +1 -1
  16. package/dist/apis/contacts/types.d.ts +76 -2
  17. package/dist/apis/contacts/types.d.ts.map +1 -1
  18. package/dist/apis/files/FilesApi.cjs +13 -1
  19. package/dist/apis/files/FilesApi.cjs.map +1 -1
  20. package/dist/apis/files/FilesApi.d.ts +12 -2
  21. package/dist/apis/files/FilesApi.d.ts.map +1 -1
  22. package/dist/apis/files/FilesApi.mjs +13 -1
  23. package/dist/apis/files/FilesApi.mjs.map +1 -1
  24. package/dist/apis/files/types.d.ts +19 -1
  25. package/dist/apis/files/types.d.ts.map +1 -1
  26. package/dist/apis/notifications/types.d.ts +6 -3
  27. package/dist/apis/notifications/types.d.ts.map +1 -1
  28. package/dist/apis/services/ServicesApi.cjs +10 -0
  29. package/dist/apis/services/ServicesApi.cjs.map +1 -1
  30. package/dist/apis/services/ServicesApi.d.ts +9 -1
  31. package/dist/apis/services/ServicesApi.d.ts.map +1 -1
  32. package/dist/apis/services/ServicesApi.mjs +10 -0
  33. package/dist/apis/services/ServicesApi.mjs.map +1 -1
  34. package/dist/apis/services/types.d.ts +10 -0
  35. package/dist/apis/services/types.d.ts.map +1 -1
  36. package/dist/apis/stickers/types.d.ts +3 -0
  37. package/dist/apis/stickers/types.d.ts.map +1 -1
  38. package/dist/core/BaseReadApi.cjs +2 -2
  39. package/dist/core/BaseReadApi.cjs.map +1 -1
  40. package/dist/core/BaseReadApi.d.ts +2 -2
  41. package/dist/core/BaseReadApi.mjs +2 -2
  42. package/dist/core/BaseReadApi.mjs.map +1 -1
  43. package/package.json +1 -1
  44. package/src/apis/authHistory/AuthHistoryApi.test.ts +16 -0
  45. package/src/apis/authHistory/AuthHistoryApi.ts +10 -2
  46. package/src/apis/authHistory/types.ts +6 -1
  47. package/src/apis/contacts/ContactsApi.test.ts +13 -0
  48. package/src/apis/contacts/ContactsApi.ts +23 -1
  49. package/src/apis/contacts/types.ts +83 -1
  50. package/src/apis/files/FilesApi.test.ts +21 -0
  51. package/src/apis/files/FilesApi.ts +19 -1
  52. package/src/apis/files/types.ts +20 -1
  53. package/src/apis/notifications/types.ts +6 -3
  54. package/src/apis/payloads.test-d.ts +60 -2
  55. package/src/apis/services/ServicesApi.test.ts +11 -0
  56. package/src/apis/services/ServicesApi.ts +21 -0
  57. package/src/apis/services/types.ts +12 -0
  58. package/src/apis/stickers/types.ts +3 -0
  59. package/src/core/BaseReadApi.ts +2 -2
@@ -1,10 +1,11 @@
1
+ import type { CustomField, CustomFieldType } from '../customFields/types'
1
2
  import type { Department } from '../departments/types'
2
3
  import type { Person } from '../people/types'
3
4
  import type { Service } from '../services/types'
4
5
  import type { Tag } from '../tags/types'
5
6
  import type { Ticket } from '../tickets/types'
6
7
  import type { User } from '../users/types'
7
- import type { IncludeItem, ListQuery, WhereClause } from '../../core/types'
8
+ import type { GetByIdQuery, IncludeItem, ListQuery, WhereClause } from '../../core/types'
8
9
 
9
10
  export type ContactStatus = 'online' | 'offline' | 'absent'
10
11
  export type ContactOrigin = 'api' | 'app' | 'campaign' | 'driver' | 'import' | 'web'
@@ -95,6 +96,56 @@ export type ContactDataBlock = {
95
96
  description: string
96
97
  }
97
98
 
99
+ /**
100
+ * A value of a custom field of the contact, as `include: ['customFieldValues']` answers it (the
101
+ * stored row).
102
+ */
103
+ export type CustomFieldValue = {
104
+ id: string
105
+ customFieldId: string
106
+ /** The contact. */
107
+ relatedId: string
108
+ relatedType: string | null
109
+ /**
110
+ * The value as stored: text, or JSON text for the composite types (`date-range`, `hour-range`,
111
+ * `numeric`, `monetary`, `phone`, `postal-code`).
112
+ */
113
+ value: string
114
+ createdAt: string
115
+ updatedAt: string
116
+ deletedAt: string | null
117
+ // relationships
118
+ customField?: CustomField
119
+ }
120
+
121
+ /**
122
+ * A custom field of the contact as `formatCustomFields: true` answers it, keyed by the name of the
123
+ * field in `Contact.customFields`.
124
+ */
125
+ export type FormattedCustomField = {
126
+ name: string
127
+ /** The value, parsed from JSON for the composite types. */
128
+ value: unknown
129
+ /** Id of the value. */
130
+ id: string
131
+ type: CustomFieldType
132
+ /** Id of the field. */
133
+ fieldId: string
134
+ /** When the field was created. */
135
+ createdAt: string
136
+ }
137
+
138
+ /** A custom field value sent in the create or update of a contact. */
139
+ export type ContactCustomFieldInput = {
140
+ /**
141
+ * Id of the custom field (`CustomField.id`, `FormattedCustomField.fieldId` or
142
+ * `CustomFieldValue.customFieldId`), not the id of the value.
143
+ */
144
+ id: string
145
+ /** Text, or an object for the composite types (stored as JSON). */
146
+ value: unknown
147
+ }
148
+
98
149
  export type Contact = {
99
150
  id: string
100
151
  idFromService: string | null
@@ -139,6 +190,13 @@ export type Contact = {
139
190
  person?: Person | null
140
191
  tags?: Tag[]
141
192
  currentTicket?: Ticket | null
193
+ /** With `include: ['customFieldValues']`, unless `formatCustomFields` is set. */
194
+ customFieldValues?: CustomFieldValue[]
195
+ /**
196
+ * With `formatCustomFields: true` and the include
197
+ * `{ model: 'customFieldValues', include: ['customField'] }`: the values keyed by the field name.
198
+ */
199
+ customFields?: Record<string, FormattedCustomField>
142
200
  }
143
201
 
144
202
  export type ContactRelationships =
@@ -148,6 +206,23 @@ export type ContactRelationships =
148
206
  | 'person'
149
207
  | 'tags'
150
208
  | 'currentTicket'
209
+ | 'customFieldValues'
210
+ | 'customFields'
211
+
212
+ type FormatCustomFieldsOption = {
213
+ /**
214
+ * Answers `customFields` keyed by the field name instead of the raw `customFieldValues` (with the
215
+ * include `{ model: 'customFieldValues', include: ['customField'] }`; without the nested
216
+ * `customField`, `customFields` comes empty).
217
+ */
218
+ formatCustomFields?: boolean
219
+ }
220
+
221
+ /** Query of `ContactsApi.getById`. */
222
+ export type ContactGetByIdQuery = GetByIdQuery<Contact> & FormatCustomFieldsOption
223
+
224
+ /** Query of `ContactsApi.getMany` and `getOne`. */
225
+ export type ContactListQuery = ListQuery<Contact> & FormatCustomFieldsOption
151
226
 
152
227
  export type CreateContactPayload = {
153
228
  name?: string
@@ -160,6 +235,8 @@ export type CreateContactPayload = {
160
235
  data?: ContactData
161
236
  note?: string
162
237
  origin?: ContactOrigin
238
+ /** Values of the custom fields of the account. An id that is not a custom field answers 400. */
239
+ customFields?: ContactCustomFieldInput[]
163
240
  }
164
241
 
165
242
  export type UpdateContactPayload = {
@@ -174,6 +251,11 @@ export type UpdateContactPayload = {
174
251
  block?: boolean
175
252
  /** Tag IDs to associate with the contact. */
176
253
  tagIds?: string[]
254
+ /**
255
+ * Values of the custom fields of the account: a value of the same field is updated. An id that is
256
+ * not a custom field is skipped.
257
+ */
258
+ customFields?: ContactCustomFieldInput[]
177
259
  }
178
260
 
179
261
  /** Result of `ContactsApi.summarizeTicket`. */
@@ -97,6 +97,27 @@ describe('FilesApi', () => {
97
97
  })
98
98
  })
99
99
 
100
+ describe('duplicateFile', () => {
101
+ it('calls client.post with /files/duplicate-file and resolves the id of the copy', async () => {
102
+ vi.mocked(client.post).mockResolvedValue('file-2')
103
+ const result = await api.duplicateFile({ fileId: 'file-1' })
104
+ expect(client.post).toHaveBeenCalledWith(
105
+ '/files/duplicate-file',
106
+ { fileId: 'file-1' },
107
+ undefined,
108
+ )
109
+ expect(result).toBe('file-2')
110
+ })
111
+
112
+ it('sends urlToUpload and serviceId in the body and forwards custom headers', async () => {
113
+ vi.mocked(client.post).mockResolvedValue('file-1')
114
+ const headers = { 'X-Req': 'r-1' }
115
+ const body = { fileId: 'file-1', urlToUpload: URL_TO_UPLOAD, serviceId: 'svc-1' }
116
+ await api.duplicateFile(body, headers)
117
+ expect(client.post).toHaveBeenCalledWith('/files/duplicate-file', body, headers)
118
+ })
119
+ })
120
+
100
121
  describe('deleteById', () => {
101
122
  it('calls client.delete with /files/:id and resolves { success }', async () => {
102
123
  vi.mocked(client.delete).mockResolvedValue({ success: true })
@@ -9,6 +9,7 @@ import type {
9
9
  CreateToUploadPayload,
10
10
  CreateToUploadResult,
11
11
  DeleteFileResult,
12
+ DuplicateFilePayload,
12
13
  } from './types'
13
14
 
14
15
  /**
@@ -85,7 +86,8 @@ export class FilesApi extends BaseCrudApi<
85
86
  * Creates the record of a file and returns the signed URL to upload its content: send the
86
87
  * content with {@link upload}. An audio that must be converted (a voice message for the
87
88
  * connection) is sent in `base64` instead, and then no `urlToUpload` is returned. `name` and
88
- * `mimetype` are required (500 without them; an audio without `name` is named `recording`).
89
+ * `mimetype` are required (500 without them; an audio without `name` is named `recording`). With
90
+ * `serviceId`, an extension the account blocks for the type of the connection answers 403.
89
91
  */
90
92
  createToUpload(
91
93
  body: CreateToUploadPayload,
@@ -98,6 +100,22 @@ export class FilesApi extends BaseCrudApi<
98
100
  )
99
101
  }
100
102
 
103
+ /**
104
+ * Copies the content of a file. With `urlToUpload` (the signed URL of a record created by
105
+ * {@link createToUpload} with the same `mimetype` as the file copied), the content is sent to that
106
+ * record and the answer is the `fileId` given; without it, a new record (`attachedType`
107
+ * `'message.file'`) is created with the copied content and its id is the answer, and with
108
+ * `serviceId` an extension the account blocks for the type of the connection answers 403. An
109
+ * unknown `fileId` answers 500.
110
+ */
111
+ duplicateFile(body: DuplicateFilePayload, headers?: Record<string, string>): Promise<string> {
112
+ return this.client.post<string, DuplicateFilePayload>(
113
+ `${this.basePath}/duplicate-file`,
114
+ body,
115
+ headers,
116
+ )
117
+ }
118
+
101
119
  /**
102
120
  * Uploads the content of a file to the `urlToUpload` returned by {@link createToUpload}. The URL
103
121
  * is signed by the storage, so the request goes straight to it, without the API token (the
@@ -74,7 +74,10 @@ export type CreateToUploadPayload = {
74
74
  mimetype: string
75
75
  /** What the file is attached to, e.g. `'message.file'` (default), `'campaign.file'`, `'quickreply.file'`. */
76
76
  attachedType?: string
77
- /** Connection that will send the file (used to decide the audio conversion). */
77
+ /**
78
+ * Connection that will send the file: used to decide the audio conversion and to apply the
79
+ * extensions the account blocks for its type (403).
80
+ */
78
81
  serviceId?: string
79
82
  /** Audio sent as a voice message (default `true`). */
80
83
  isPtt?: boolean
@@ -93,6 +96,22 @@ export type CreateToUploadResult = {
93
96
  mimetype: string
94
97
  }
95
98
 
99
+ /** Body of `FilesApi.duplicateFile`. */
100
+ export type DuplicateFilePayload = {
101
+ /** The file whose content is copied. */
102
+ fileId: string
103
+ /**
104
+ * Signed URL of a record created by `FilesApi.createToUpload`, with the same `mimetype` as the file
105
+ * copied, to receive the content. Without it, a new record is created with the copied content.
106
+ */
107
+ urlToUpload?: string
108
+ /**
109
+ * Connection that will send the new record (used only without `urlToUpload`): the extensions the
110
+ * account blocks for its type apply.
111
+ */
112
+ serviceId?: string
113
+ }
114
+
96
115
  /** Result of `FilesApi.deleteById`. */
97
116
  export type DeleteFileResult = {
98
117
  success: boolean
@@ -78,8 +78,10 @@ export type Notification = {
78
78
  label: string | null
79
79
  config: NotificationConfig | null
80
80
  attachedType: string | null
81
- attachedId: string | null
82
- notificationReadId: string | null
81
+ /** @deprecated Not answered: the model has no such column, so the transformer never sends it. */
82
+ attachedId?: string | null
83
+ /** @deprecated Not answered: the model has no such column, so the transformer never sends it. */
84
+ notificationReadId?: string | null
83
85
  contactId: string | null
84
86
  userId: string
85
87
  accountId: string
@@ -93,7 +95,8 @@ export type Notification = {
93
95
  schedule?: Schedule | null
94
96
  /** @deprecated Not served: the model has no such association, so it never comes. */
95
97
  contact?: Contact | null
96
- notificationRead?: NotificationRead | null
98
+ /** The read receipts of the notification (a list: `[]` while it was never read). */
99
+ notificationRead?: NotificationRead[]
97
100
  }
98
101
 
99
102
  /** The backend serves `user`, `schedule` and `notificationRead` (`contact` is never answered). */
@@ -54,7 +54,14 @@ import { AcceptanceTermsApi } from './acceptanceTerms/AcceptanceTermsApi'
54
54
  import { CategoriesApi } from './categories/CategoriesApi'
55
55
  import { ContactBlockListsApi } from './contactBlockLists/ContactBlockListsApi'
56
56
  import { ContactBlockListsControlsApi } from './contactBlockListsControls/ContactBlockListsControlsApi'
57
- import type { SummarizeTicketResult } from './contacts/types'
57
+ import type {
58
+ Contact,
59
+ CustomFieldValue,
60
+ FormattedCustomField,
61
+ SummarizeTicketResult,
62
+ } from './contacts/types'
63
+ import { AuthHistoryApi } from './authHistory/AuthHistoryApi'
64
+ import type { AuthHistoryEvent } from './authHistory/types'
58
65
  import { CustomFieldsApi } from './customFields/CustomFieldsApi'
59
66
  import { DepartmentsApi } from './departments/DepartmentsApi'
60
67
  import type { Department } from './departments/types'
@@ -71,7 +78,11 @@ import { KnowledgeBaseApi } from './knowledgeBase/KnowledgeBaseApi'
71
78
  import { KnowledgeBaseItemApi } from './knowledgeBaseItem/KnowledgeBaseItemApi'
72
79
  import { MessagesApi } from './messages/MessagesApi'
73
80
  import { NotificationsApi } from './notifications/NotificationsApi'
74
- import type { NotificationCountsResponse } from './notifications/types'
81
+ import type {
82
+ Notification,
83
+ NotificationCountsResponse,
84
+ NotificationRead,
85
+ } from './notifications/types'
75
86
  import { AbsenceApi } from './absence/AbsenceApi'
76
87
  import type { AbsenceReportItem, UnlockAbsenceResponse } from './absence/types'
77
88
  import { ActivityLogApi } from './activityLog/ActivityLogApi'
@@ -749,3 +760,50 @@ expectType<Promise<Sticker>>(new StickersApi(client).create({ base64: 'iVBOR', n
749
760
  expectError(new StickersApi(client).create({ type: 'created' }))
750
761
  expectType<Promise<'OK' | false>>(new StickersApi(client).remove('s-1'))
751
762
  expectAssignable<Parameters<StickersApi['getMany']>[0]>({ include: ['file'] })
763
+
764
+ // ── QARC-1010 (4.13.1): the shapes observed on qa-rc ─────────────────
765
+
766
+ declare const notification: Notification
767
+ expectType<NotificationRead[] | undefined>(notification.notificationRead)
768
+ expectType<string | null | undefined>(notification.attachedId)
769
+ declare const sticker: Sticker
770
+ expectAssignable<Sticker>({ ...sticker, originMessage: null })
771
+ expectAssignable<Promise<{ message: string }>>(
772
+ new ServicesApi(client).accessManagement('s-1', { userIds: ['u-1'], deny: true }),
773
+ )
774
+ expectAssignable<Awaited<ReturnType<ServicesApi['accessManagement']>>>({
775
+ message: '2 users allowed access.',
776
+ })
777
+ expectNotAssignable<Awaited<ReturnType<ServicesApi['accessManagement']>>>({ message: 'denied' })
778
+ expectError(new ServicesApi(client).accessManagement('s-1', { userIds: ['u-1'] }))
779
+
780
+ // ── QARC-1015 (4.15.0): custom fields of the contact, file copy, logout event ─────
781
+
782
+ expectAssignable<Parameters<ContactsApi['create']>[0]>({
783
+ serviceId: 's-1',
784
+ customFields: [
785
+ { id: 'f-1', value: 'text' },
786
+ { id: 'f-2', value: { value: '10,50', identifier: 'BRL', min: '', max: '' } },
787
+ ],
788
+ })
789
+ expectAssignable<Parameters<ContactsApi['updateById']>[1]>({
790
+ customFields: [{ id: 'f-1', value: 'new' }],
791
+ })
792
+ expectError(
793
+ new ContactsApi(client).create({ serviceId: 's-1', customFields: [{ value: 'no id' }] }),
794
+ )
795
+ expectAssignable<Parameters<ContactsApi['getById']>[1]>({
796
+ include: [{ model: 'customFieldValues', include: ['customField'] }],
797
+ formatCustomFields: true,
798
+ })
799
+ declare const contactWithFields: Contact
800
+ expectType<CustomFieldValue[] | undefined>(contactWithFields.customFieldValues)
801
+ expectType<Record<string, FormattedCustomField> | undefined>(contactWithFields.customFields)
802
+ expectAssignable<Parameters<ContactsApi['getMany']>[0]>({
803
+ include: [{ model: 'customFieldValues', include: ['customField'] }],
804
+ formatCustomFields: true,
805
+ })
806
+ expectType<Promise<string>>(new FilesApi(client).duplicateFile({ fileId: 'file-1' }))
807
+ expectError(new FilesApi(client).duplicateFile({ urlToUpload: 'https://storage' }))
808
+ expectType<Promise<void>>(new AuthHistoryApi(client).registerLogout())
809
+ expectAssignable<AuthHistoryEvent>('2fa_disabled')
@@ -91,4 +91,15 @@ describe('ServicesApi', () => {
91
91
  })
92
92
  },
93
93
  )
94
+
95
+ describe('accessManagement', () => {
96
+ it('posts the users and the deny flag to /services/:id/access-management', async () => {
97
+ vi.mocked(client.post).mockResolvedValue({ message: '1 users denied access.' })
98
+ const body = { userIds: ['u-1'], deny: true }
99
+ await expect(api.accessManagement('svc-1', body)).resolves.toEqual({
100
+ message: '1 users denied access.',
101
+ })
102
+ expect(client.post).toHaveBeenCalledWith('/services/svc-1/access-management', body, undefined)
103
+ })
104
+ })
94
105
  })
@@ -6,6 +6,8 @@ import type {
6
6
  ServiceLimits,
7
7
  UpdateServicePayload,
8
8
  ArchiveServicePayload,
9
+ ServiceAccessManagementPayload,
10
+ ServiceAccessManagementResponse,
9
11
  } from './types'
10
12
 
11
13
  export class ServicesApi extends ArchivableCrudApi<
@@ -109,4 +111,23 @@ export class ServicesApi extends ArchivableCrudApi<
109
111
  restart(id: string, headers?: Record<string, string>): Promise<'OK'> {
110
112
  return this.client.post<'OK'>(`${this.basePath}/${id}/restart`, undefined, headers)
111
113
  }
114
+
115
+ /**
116
+ * Denies the connection to users (`deny: true`) or gives it back (`deny: false`); the admins are skipped. Denying
117
+ * transfers the open tickets of those users in the connection. The rows are read with
118
+ * `ServiceAccessManagementApi.getMany`. An unknown connection answers 404 `Service not found.`; a missing
119
+ * `userIds` or a `deny` that is not a boolean answers 400.
120
+ * @permissions users.update AND services.operate
121
+ */
122
+ accessManagement(
123
+ id: string,
124
+ body: ServiceAccessManagementPayload,
125
+ headers?: Record<string, string>,
126
+ ): Promise<ServiceAccessManagementResponse> {
127
+ return this.client.post<ServiceAccessManagementResponse, ServiceAccessManagementPayload>(
128
+ `${this.basePath}/${id}/access-management`,
129
+ body,
130
+ headers,
131
+ )
132
+ }
112
133
  }
@@ -292,3 +292,15 @@ export type ServiceLimit = {
292
292
 
293
293
  /** Connections used and allowed by the account plan, by connection type. */
294
294
  export type ServiceLimits = Partial<Record<ServiceType, ServiceLimit>>
295
+
296
+ /** The body of `accessManagement`: the users to deny (or give back) the connection to. */
297
+ export type ServiceAccessManagementPayload = {
298
+ userIds: string[]
299
+ /** `true` denies the connection to the users (their open tickets in it are transferred); `false` gives it back. */
300
+ deny: boolean
301
+ }
302
+
303
+ /** What `accessManagement` answers: how many users were denied or given back the connection (no admin is counted). */
304
+ export type ServiceAccessManagementResponse = {
305
+ message: `${number} users denied access.` | `${number} users allowed access.`
306
+ }
@@ -1,4 +1,5 @@
1
1
  import type { File } from '../files/types'
2
+ import type { Message } from '../messages/types'
2
3
  import type { User } from '../users/types'
3
4
 
4
5
  /** A sticker of the account (every user of the account sees it). */
@@ -19,6 +20,8 @@ export type Sticker = {
19
20
  user?: User
20
21
  /** The image (webp) of the sticker. */
21
22
  file?: File
23
+ /** The message the sticker came from (`add`); `null` on a sticker uploaded with `create`. */
24
+ originMessage?: Message | null
22
25
  }
23
26
 
24
27
  /** The backend serves only `file` (`user` is dropped). */
@@ -4,8 +4,8 @@ import type { GetByIdQuery, HasGetById, HasGetMany, ListQuery, Paginated } from
4
4
 
5
5
  /**
6
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).
7
+ * `BaseCrudApi`, and of the resources the backend serves for reading, plus their own actions (e.g.
8
+ * permissions, auth history).
9
9
  */
10
10
  export class BaseReadApi<TResponse, TQuery extends ListQuery<TResponse> = ListQuery<TResponse>>
11
11
  extends BaseApi