@ikatec/digisac-api-sdk 4.15.0 → 4.16.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 (189) hide show
  1. package/README.md +3 -1
  2. package/dist/apis/absence/AbsenceApi.cjs +2 -2
  3. package/dist/apis/absence/AbsenceApi.cjs.map +1 -1
  4. package/dist/apis/absence/AbsenceApi.d.ts +2 -2
  5. package/dist/apis/absence/AbsenceApi.mjs +2 -2
  6. package/dist/apis/absence/AbsenceApi.mjs.map +1 -1
  7. package/dist/apis/absence/types.d.ts +1 -1
  8. package/dist/apis/answers/AnswersApi.cjs +23 -2
  9. package/dist/apis/answers/AnswersApi.cjs.map +1 -1
  10. package/dist/apis/answers/AnswersApi.d.ts +16 -2
  11. package/dist/apis/answers/AnswersApi.d.ts.map +1 -1
  12. package/dist/apis/answers/AnswersApi.mjs +23 -2
  13. package/dist/apis/answers/AnswersApi.mjs.map +1 -1
  14. package/dist/apis/answers/types.d.ts +1 -2
  15. package/dist/apis/answers/types.d.ts.map +1 -1
  16. package/dist/apis/bots/BotsApi.cjs +16 -7
  17. package/dist/apis/bots/BotsApi.cjs.map +1 -1
  18. package/dist/apis/bots/BotsApi.d.ts +13 -7
  19. package/dist/apis/bots/BotsApi.d.ts.map +1 -1
  20. package/dist/apis/bots/BotsApi.mjs +16 -7
  21. package/dist/apis/bots/BotsApi.mjs.map +1 -1
  22. package/dist/apis/bots/types.d.ts +12 -2
  23. package/dist/apis/bots/types.d.ts.map +1 -1
  24. package/dist/apis/campaigns/CampaignsApi.cjs +25 -1
  25. package/dist/apis/campaigns/CampaignsApi.cjs.map +1 -1
  26. package/dist/apis/campaigns/CampaignsApi.d.ts +16 -1
  27. package/dist/apis/campaigns/CampaignsApi.d.ts.map +1 -1
  28. package/dist/apis/campaigns/CampaignsApi.mjs +25 -1
  29. package/dist/apis/campaigns/CampaignsApi.mjs.map +1 -1
  30. package/dist/apis/campaigns/types.d.ts +98 -6
  31. package/dist/apis/campaigns/types.d.ts.map +1 -1
  32. package/dist/apis/cards/types.d.ts +5 -5
  33. package/dist/apis/categories/CategoriesApi.cjs +1 -2
  34. package/dist/apis/categories/CategoriesApi.cjs.map +1 -1
  35. package/dist/apis/categories/CategoriesApi.d.ts +1 -2
  36. package/dist/apis/categories/CategoriesApi.d.ts.map +1 -1
  37. package/dist/apis/categories/CategoriesApi.mjs +1 -2
  38. package/dist/apis/categories/CategoriesApi.mjs.map +1 -1
  39. package/dist/apis/clientFeedback/ClientFeedbackApi.cjs +2 -3
  40. package/dist/apis/clientFeedback/ClientFeedbackApi.cjs.map +1 -1
  41. package/dist/apis/clientFeedback/ClientFeedbackApi.d.ts +2 -3
  42. package/dist/apis/clientFeedback/ClientFeedbackApi.d.ts.map +1 -1
  43. package/dist/apis/clientFeedback/ClientFeedbackApi.mjs +2 -3
  44. package/dist/apis/clientFeedback/ClientFeedbackApi.mjs.map +1 -1
  45. package/dist/apis/contacts/ContactsApi.cjs +10 -6
  46. package/dist/apis/contacts/ContactsApi.cjs.map +1 -1
  47. package/dist/apis/contacts/ContactsApi.d.ts +19 -3
  48. package/dist/apis/contacts/ContactsApi.d.ts.map +1 -1
  49. package/dist/apis/contacts/ContactsApi.mjs +10 -6
  50. package/dist/apis/contacts/ContactsApi.mjs.map +1 -1
  51. package/dist/apis/contacts/types.d.ts +25 -0
  52. package/dist/apis/contacts/types.d.ts.map +1 -1
  53. package/dist/apis/customFields/types.d.ts +4 -4
  54. package/dist/apis/dashboard/DashboardApi.cjs +3 -3
  55. package/dist/apis/dashboard/DashboardApi.cjs.map +1 -1
  56. package/dist/apis/dashboard/DashboardApi.d.ts +3 -3
  57. package/dist/apis/dashboard/DashboardApi.mjs +3 -3
  58. package/dist/apis/dashboard/DashboardApi.mjs.map +1 -1
  59. package/dist/apis/dashboard/types.d.ts +11 -4
  60. package/dist/apis/dashboard/types.d.ts.map +1 -1
  61. package/dist/apis/departments/DepartmentsApi.cjs +5 -5
  62. package/dist/apis/departments/DepartmentsApi.cjs.map +1 -1
  63. package/dist/apis/departments/DepartmentsApi.d.ts +5 -5
  64. package/dist/apis/departments/DepartmentsApi.mjs +5 -5
  65. package/dist/apis/departments/DepartmentsApi.mjs.map +1 -1
  66. package/dist/apis/departments/types.d.ts +6 -0
  67. package/dist/apis/departments/types.d.ts.map +1 -1
  68. package/dist/apis/distribution/DistributionApi.cjs +3 -4
  69. package/dist/apis/distribution/DistributionApi.cjs.map +1 -1
  70. package/dist/apis/distribution/DistributionApi.d.ts +3 -4
  71. package/dist/apis/distribution/DistributionApi.d.ts.map +1 -1
  72. package/dist/apis/distribution/DistributionApi.mjs +3 -4
  73. package/dist/apis/distribution/DistributionApi.mjs.map +1 -1
  74. package/dist/apis/kanbanCards/KanbanCardsApi.cjs +32 -0
  75. package/dist/apis/kanbanCards/KanbanCardsApi.cjs.map +1 -1
  76. package/dist/apis/kanbanCards/KanbanCardsApi.d.ts +27 -1
  77. package/dist/apis/kanbanCards/KanbanCardsApi.d.ts.map +1 -1
  78. package/dist/apis/kanbanCards/KanbanCardsApi.mjs +32 -0
  79. package/dist/apis/kanbanCards/KanbanCardsApi.mjs.map +1 -1
  80. package/dist/apis/kanbanCards/types.d.ts +1 -1
  81. package/dist/apis/kanbanCards/types.d.ts.map +1 -1
  82. package/dist/apis/kanbanColumns/KanbanColumnsApi.cjs +19 -0
  83. package/dist/apis/kanbanColumns/KanbanColumnsApi.cjs.map +1 -1
  84. package/dist/apis/kanbanColumns/KanbanColumnsApi.d.ts +16 -1
  85. package/dist/apis/kanbanColumns/KanbanColumnsApi.d.ts.map +1 -1
  86. package/dist/apis/kanbanColumns/KanbanColumnsApi.mjs +19 -0
  87. package/dist/apis/kanbanColumns/KanbanColumnsApi.mjs.map +1 -1
  88. package/dist/apis/kanbanColumns/types.d.ts +4 -0
  89. package/dist/apis/kanbanColumns/types.d.ts.map +1 -1
  90. package/dist/apis/messages/MessagesApi.cjs +13 -4
  91. package/dist/apis/messages/MessagesApi.cjs.map +1 -1
  92. package/dist/apis/messages/MessagesApi.d.ts +13 -4
  93. package/dist/apis/messages/MessagesApi.d.ts.map +1 -1
  94. package/dist/apis/messages/MessagesApi.mjs +13 -4
  95. package/dist/apis/messages/MessagesApi.mjs.map +1 -1
  96. package/dist/apis/messages/types.d.ts +5 -0
  97. package/dist/apis/messages/types.d.ts.map +1 -1
  98. package/dist/apis/pipeline/types.d.ts +7 -8
  99. package/dist/apis/pipeline/types.d.ts.map +1 -1
  100. package/dist/apis/quickReplies/types.d.ts +4 -1
  101. package/dist/apis/quickReplies/types.d.ts.map +1 -1
  102. package/dist/apis/schedule/types.d.ts +41 -10
  103. package/dist/apis/schedule/types.d.ts.map +1 -1
  104. package/dist/apis/terms/TermsApi.cjs +2 -2
  105. package/dist/apis/terms/TermsApi.cjs.map +1 -1
  106. package/dist/apis/terms/TermsApi.d.ts +2 -2
  107. package/dist/apis/terms/TermsApi.mjs +2 -2
  108. package/dist/apis/terms/TermsApi.mjs.map +1 -1
  109. package/dist/apis/tickets/TicketsApi.cjs +18 -8
  110. package/dist/apis/tickets/TicketsApi.cjs.map +1 -1
  111. package/dist/apis/tickets/TicketsApi.d.ts +15 -8
  112. package/dist/apis/tickets/TicketsApi.d.ts.map +1 -1
  113. package/dist/apis/tickets/TicketsApi.mjs +18 -8
  114. package/dist/apis/tickets/TicketsApi.mjs.map +1 -1
  115. package/dist/apis/tickets/types.d.ts +4 -1
  116. package/dist/apis/tickets/types.d.ts.map +1 -1
  117. package/dist/apis/timetable/TimetableApi.cjs +6 -0
  118. package/dist/apis/timetable/TimetableApi.cjs.map +1 -1
  119. package/dist/apis/timetable/TimetableApi.d.ts +6 -0
  120. package/dist/apis/timetable/TimetableApi.d.ts.map +1 -1
  121. package/dist/apis/timetable/TimetableApi.mjs +6 -0
  122. package/dist/apis/timetable/TimetableApi.mjs.map +1 -1
  123. package/dist/apis/timetable/types.d.ts +8 -0
  124. package/dist/apis/timetable/types.d.ts.map +1 -1
  125. package/dist/apis/users/UsersApi.cjs +11 -3
  126. package/dist/apis/users/UsersApi.cjs.map +1 -1
  127. package/dist/apis/users/UsersApi.d.ts +10 -4
  128. package/dist/apis/users/UsersApi.d.ts.map +1 -1
  129. package/dist/apis/users/UsersApi.mjs +11 -3
  130. package/dist/apis/users/UsersApi.mjs.map +1 -1
  131. package/dist/apis/users/types.d.ts +26 -0
  132. package/dist/apis/users/types.d.ts.map +1 -1
  133. package/dist/apis/webhooks/types.d.ts +51 -6
  134. package/dist/apis/webhooks/types.d.ts.map +1 -1
  135. package/dist/core/errors.cjs +4 -1
  136. package/dist/core/errors.cjs.map +1 -1
  137. package/dist/core/errors.d.ts +4 -1
  138. package/dist/core/errors.d.ts.map +1 -1
  139. package/dist/core/errors.mjs +4 -1
  140. package/dist/core/errors.mjs.map +1 -1
  141. package/package.json +1 -1
  142. package/src/apis/absence/AbsenceApi.ts +2 -2
  143. package/src/apis/absence/types.ts +1 -1
  144. package/src/apis/answers/AnswersApi.test.ts +32 -0
  145. package/src/apis/answers/AnswersApi.ts +38 -2
  146. package/src/apis/answers/types.ts +1 -2
  147. package/src/apis/bots/BotsApi.test.ts +16 -0
  148. package/src/apis/bots/BotsApi.ts +25 -7
  149. package/src/apis/bots/types.ts +12 -2
  150. package/src/apis/campaigns/CampaignsApi.test.ts +48 -0
  151. package/src/apis/campaigns/CampaignsApi.ts +31 -2
  152. package/src/apis/campaigns/types.ts +101 -3
  153. package/src/apis/cards/types.ts +5 -5
  154. package/src/apis/categories/CategoriesApi.ts +1 -2
  155. package/src/apis/clientFeedback/ClientFeedbackApi.ts +2 -3
  156. package/src/apis/contacts/ContactsApi.test.ts +36 -2
  157. package/src/apis/contacts/ContactsApi.ts +41 -3
  158. package/src/apis/contacts/types.ts +28 -0
  159. package/src/apis/creditMovements/CreditMovementsApi.test.ts +1 -1
  160. package/src/apis/customFields/types.ts +4 -4
  161. package/src/apis/dashboard/DashboardApi.test-d.ts +9 -0
  162. package/src/apis/dashboard/DashboardApi.ts +3 -3
  163. package/src/apis/dashboard/types.ts +11 -4
  164. package/src/apis/departments/DepartmentsApi.ts +5 -5
  165. package/src/apis/departments/types.ts +13 -0
  166. package/src/apis/distribution/DistributionApi.ts +3 -4
  167. package/src/apis/kanbanCards/KanbanCardsApi.test.ts +21 -0
  168. package/src/apis/kanbanCards/KanbanCardsApi.ts +47 -1
  169. package/src/apis/kanbanCards/types.ts +1 -1
  170. package/src/apis/kanbanColumns/KanbanColumnsApi.test.ts +26 -0
  171. package/src/apis/kanbanColumns/KanbanColumnsApi.ts +31 -0
  172. package/src/apis/kanbanColumns/types.ts +5 -0
  173. package/src/apis/messages/MessagesApi.ts +13 -4
  174. package/src/apis/messages/types.ts +5 -0
  175. package/src/apis/payloads.test-d.ts +206 -4
  176. package/src/apis/pipeline/types.ts +7 -8
  177. package/src/apis/queryFormat.test.ts +13 -0
  178. package/src/apis/quickReplies/types.ts +4 -1
  179. package/src/apis/schedule/types.ts +42 -10
  180. package/src/apis/terms/TermsApi.ts +2 -2
  181. package/src/apis/tickets/TicketsApi.ts +28 -8
  182. package/src/apis/tickets/types.ts +4 -1
  183. package/src/apis/timetable/TimetableApi.ts +6 -0
  184. package/src/apis/timetable/types.ts +9 -0
  185. package/src/apis/users/UsersApi.test.ts +17 -0
  186. package/src/apis/users/UsersApi.ts +21 -3
  187. package/src/apis/users/types.ts +28 -0
  188. package/src/apis/webhooks/types.ts +53 -6
  189. package/src/core/errors.ts +4 -1
@@ -1 +1 @@
1
- {"version":3,"file":"errors.cjs","names":[],"sources":["../../src/core/errors.ts"],"sourcesContent":["/**\n * A field-level error from the backend `validate` middleware — one entry per failed rule.\n */\nexport interface FieldError {\n /** Where the field was read from: `body`, `params`, `query`, `headers` or `cookies`. */\n location: string\n /** The field name, e.g. `name`. */\n field: string\n /** `<location>.<field>`, e.g. `body.name`. */\n path: string\n message: string\n /** The failed rule, e.g. `required` or `hasLengthLesserThanOrEqual`. */\n code: string\n}\n\n/**\n * The backend error name sent as `error` in the body (`ApiError.errorClass`). Known names are listed\n * for autocomplete; any other string is still accepted. `HttpError` is the generic one: most 400s\n * and every unexpected 500.\n */\nexport type ErrorClassName =\n | 'HttpError'\n | 'ValidationError'\n | 'BadRequestHttpError'\n | 'UnauthorizedHttpError'\n | 'PaymentRequired'\n | 'ForbiddenHttpError'\n | 'NotFoundHttpError'\n | 'TooManyRequestsHttpError'\n | (string & {})\n\nexport interface ApiErrorOptions {\n /** Response headers. */\n headers?: Headers\n /** The parsed JSON error body, or the raw text when it is not JSON. */\n body?: unknown\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Flattens the backend `errors` object — `{ <location>: { <field>: { messages: string[], types: string[] } } }`\n * — into one {@link FieldError} per message. Returns `null` when there are no field errors.\n */\nexport function parseFieldErrors(errors: unknown): FieldError[] | null {\n if (!isRecord(errors)) {\n return null\n }\n\n const parsed: FieldError[] = []\n for (const [location, fields] of Object.entries(errors)) {\n if (!isRecord(fields)) {\n continue\n }\n for (const [field, detail] of Object.entries(fields)) {\n if (!isRecord(detail)) {\n continue\n }\n const messages = Array.isArray(detail.messages) ? detail.messages.map(String) : []\n const types = Array.isArray(detail.types) ? detail.types.map(String) : []\n messages.forEach((message, index) => {\n parsed.push({\n location,\n field,\n path: `${location}.${field}`,\n message,\n code: types[index] ?? types[0] ?? '',\n })\n })\n }\n }\n\n return parsed.length ? parsed : null\n}\n\n/** Base class of every error thrown by the SDK. */\nexport class DigisacError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'DigisacError'\n }\n}\n\n/** The request never got an HTTP response (network failure, DNS, connection refused, ...). */\nexport class ApiConnectionError extends DigisacError {\n constructor(message = 'Connection error.', options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'ApiConnectionError'\n }\n}\n\n/**\n * The API answered with a non-2xx status. Thrown as the subclass matching the status\n * (`NotFoundError`, `BadRequestError`, ...) — or `ValidationError` for a 400 field validation\n * failure — so callers can narrow with `instanceof`.\n */\nexport class ApiError extends DigisacError {\n /** The backend error name (`error` in the body), e.g. `ValidationError` or `PaymentRequired`. */\n public readonly errorClass?: ErrorClassName | undefined\n public readonly status?: number | undefined\n /** Field errors of a 400 `ValidationError`; `null` when the response carries none. */\n public readonly validationErrors: FieldError[] | null\n public readonly headers?: Headers | undefined\n /** The parsed JSON error body (`{ error, message, status, errors?, extra? }`), or the raw text. */\n public readonly body?: unknown\n\n constructor(\n message: string,\n errorClass?: ErrorClassName,\n status?: number,\n validationErrors: FieldError[] | null = null,\n options: ApiErrorOptions = {},\n ) {\n super(message)\n this.name = 'ApiError'\n this.errorClass = errorClass\n this.status = status\n this.validationErrors = validationErrors\n this.headers = options.headers\n this.body = options.body\n }\n\n /**\n * Builds the `ApiError` subclass for a response and its (already read) body: `ValidationError`\n * for a 400 `ValidationError`, else the class of the status.\n */\n static fromResponse(\n status: number,\n statusText: string,\n headers: Headers,\n body: unknown,\n ): ApiError {\n let message = `HTTP Error ${status}: ${statusText}`\n let errorClass: string | undefined\n let validationErrors: FieldError[] | null = null\n\n if (isRecord(body)) {\n const apiMessage = typeof body.message === 'string' ? body.message : undefined\n errorClass = typeof body.error === 'string' ? body.error : undefined\n message = apiMessage || errorClass || message\n validationErrors = parseFieldErrors(body.errors)\n }\n\n const ErrorClass = errorClassFor(status, errorClass)\n return new ErrorClass(message, errorClass, status, validationErrors, { headers, body })\n }\n}\n\n/** 400 (see {@link ValidationError} for field validation failures). */\nexport class BadRequestError extends ApiError {\n override name = 'BadRequestError'\n}\n\n/**\n * 400 `ValidationError` — the request failed field validation. `validationErrors` is always an\n * array here (empty when the backend sent no field details).\n */\nexport class ValidationError extends BadRequestError {\n override name = 'ValidationError'\n declare public readonly validationErrors: FieldError[]\n\n constructor(\n message: string,\n errorClass?: ErrorClassName,\n status?: number,\n validationErrors: FieldError[] | null = null,\n options: ApiErrorOptions = {},\n ) {\n super(message, errorClass, status, validationErrors ?? [], options)\n }\n}\n\n/** 401 — missing or invalid access token. */\nexport class AuthenticationError extends ApiError {\n override name = 'AuthenticationError'\n}\n\n/** 402 — the account is out of credits or over a plan limit. */\nexport class PaymentRequiredError extends ApiError {\n override name = 'PaymentRequiredError'\n}\n\n/** 403 — the user lacks the permission for this action. */\nexport class PermissionDeniedError extends ApiError {\n override name = 'PermissionDeniedError'\n}\n\n/** 404 */\nexport class NotFoundError extends ApiError {\n override name = 'NotFoundError'\n}\n\n/** 409 */\nexport class ConflictError extends ApiError {\n override name = 'ConflictError'\n}\n\n/** 422 */\nexport class UnprocessableEntityError extends ApiError {\n override name = 'UnprocessableEntityError'\n}\n\n/** 429 */\nexport class RateLimitError extends ApiError {\n override name = 'RateLimitError'\n\n /**\n * Seconds to wait before retrying, or `null` when the response does not say.\n *\n * Reads the standard `Retry-After` header (delay seconds or HTTP date) first, then the\n * `X-RateLimit-Reset` header the Digisac backend sends (Unix time in seconds).\n */\n get retryAfter(): number | null {\n return (\n parseRetryAfter(this.headers?.get('Retry-After')) ??\n parseRateLimitReset(this.headers?.get('X-RateLimit-Reset'))\n )\n }\n}\n\nconst secondsUntil = (epochMs: number): number =>\n Math.max(0, Math.ceil((epochMs - Date.now()) / 1000))\n\n/** `Retry-After`: delay seconds or an HTTP date. */\nfunction parseRetryAfter(value: string | null | undefined): number | null {\n if (!value?.trim()) {\n return null\n }\n const seconds = Number(value)\n if (Number.isFinite(seconds)) {\n return Math.max(0, seconds)\n }\n const date = Date.parse(value)\n return Number.isNaN(date) ? null : secondsUntil(date)\n}\n\n/**\n * `X-RateLimit-Reset`: Unix time in seconds (what the Digisac backend sends). Small values are\n * read as delay seconds instead, the other convention in use for this header.\n */\nfunction parseRateLimitReset(value: string | null | undefined): number | null {\n if (!value?.trim()) {\n return null\n }\n const reset = Number(value)\n if (!Number.isFinite(reset)) {\n return null\n }\n // Anything before 2001-09-09 (1e9 s) can't be a reset time, so it is a delay.\n return reset < 1e9 ? Math.max(0, reset) : secondsUntil(reset * 1000)\n}\n\n/** 5xx */\nexport class InternalServerError extends ApiError {\n override name = 'InternalServerError'\n}\n\nfunction errorClassFor(status: number, errorClass: string | undefined): typeof ApiError {\n if (status === 400 && errorClass === 'ValidationError') {\n return ValidationError\n }\n\n switch (status) {\n case 400:\n return BadRequestError\n case 401:\n return AuthenticationError\n case 402:\n return PaymentRequiredError\n case 403:\n return PermissionDeniedError\n case 404:\n return NotFoundError\n case 409:\n return ConflictError\n case 422:\n return UnprocessableEntityError\n case 429:\n return RateLimitError\n default:\n return status >= 500 ? InternalServerError : ApiError\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"errors.cjs","names":[],"sources":["../../src/core/errors.ts"],"sourcesContent":["/**\n * A field-level error from the backend `validate` middleware — one entry per failed rule.\n */\nexport interface FieldError {\n /** Where the field was read from: `body`, `params`, `query`, `headers` or `cookies`. */\n location: string\n /** The field name, e.g. `name`. */\n field: string\n /** `<location>.<field>`, e.g. `body.name`. */\n path: string\n message: string\n /** The failed rule, e.g. `required` or `hasLengthLesserThanOrEqual`. */\n code: string\n}\n\n/**\n * The backend error name sent as `error` in the body (`ApiError.errorClass`). Known names are listed\n * for autocomplete; any other string is still accepted. `HttpError` is the generic one: most 400s\n * and every unexpected 500.\n */\nexport type ErrorClassName =\n | 'HttpError'\n | 'ValidationError'\n | 'BadRequestHttpError'\n | 'UnauthorizedHttpError'\n | 'PaymentRequired'\n | 'ForbiddenHttpError'\n | 'NotFoundHttpError'\n | 'TooManyRequestsHttpError'\n | (string & {})\n\nexport interface ApiErrorOptions {\n /** Response headers. */\n headers?: Headers\n /** The parsed JSON error body, or the raw text when it is not JSON. */\n body?: unknown\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Flattens the backend `errors` object — `{ <location>: { <field>: { messages: string[], types: string[] } } }`\n * — into one {@link FieldError} per message. Returns `null` when there are no field errors.\n */\nexport function parseFieldErrors(errors: unknown): FieldError[] | null {\n if (!isRecord(errors)) {\n return null\n }\n\n const parsed: FieldError[] = []\n for (const [location, fields] of Object.entries(errors)) {\n if (!isRecord(fields)) {\n continue\n }\n for (const [field, detail] of Object.entries(fields)) {\n if (!isRecord(detail)) {\n continue\n }\n const messages = Array.isArray(detail.messages) ? detail.messages.map(String) : []\n const types = Array.isArray(detail.types) ? detail.types.map(String) : []\n messages.forEach((message, index) => {\n parsed.push({\n location,\n field,\n path: `${location}.${field}`,\n message,\n code: types[index] ?? types[0] ?? '',\n })\n })\n }\n }\n\n return parsed.length ? parsed : null\n}\n\n/** Base class of every error thrown by the SDK. */\nexport class DigisacError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'DigisacError'\n }\n}\n\n/** The request never got an HTTP response (network failure, DNS, connection refused, ...). */\nexport class ApiConnectionError extends DigisacError {\n constructor(message = 'Connection error.', options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'ApiConnectionError'\n }\n}\n\n/**\n * The API answered with a non-2xx status. Thrown as the subclass matching the status\n * (`NotFoundError`, `BadRequestError`, ...) — or `ValidationError` for a 400 field validation\n * failure — so callers can narrow with `instanceof`.\n */\nexport class ApiError extends DigisacError {\n /** The backend error name (`error` in the body), e.g. `ValidationError` or `PaymentRequired`. */\n public readonly errorClass?: ErrorClassName | undefined\n public readonly status?: number | undefined\n /** Field errors of a 400 `ValidationError`; `null` when the response carries none. */\n public readonly validationErrors: FieldError[] | null\n public readonly headers?: Headers | undefined\n /** The parsed JSON error body (`{ error, message, status, errors?, extra? }`), or the raw text. */\n public readonly body?: unknown\n\n constructor(\n message: string,\n errorClass?: ErrorClassName,\n status?: number,\n validationErrors: FieldError[] | null = null,\n options: ApiErrorOptions = {},\n ) {\n super(message)\n this.name = 'ApiError'\n this.errorClass = errorClass\n this.status = status\n this.validationErrors = validationErrors\n this.headers = options.headers\n this.body = options.body\n }\n\n /**\n * Builds the `ApiError` subclass for a response and its (already read) body: `ValidationError`\n * for a 400 `ValidationError`, else the class of the status.\n */\n static fromResponse(\n status: number,\n statusText: string,\n headers: Headers,\n body: unknown,\n ): ApiError {\n let message = `HTTP Error ${status}: ${statusText}`\n let errorClass: string | undefined\n let validationErrors: FieldError[] | null = null\n\n if (isRecord(body)) {\n const apiMessage = typeof body.message === 'string' ? body.message : undefined\n errorClass = typeof body.error === 'string' ? body.error : undefined\n message = apiMessage || errorClass || message\n validationErrors = parseFieldErrors(body.errors)\n }\n\n const ErrorClass = errorClassFor(status, errorClass)\n return new ErrorClass(message, errorClass, status, validationErrors, { headers, body })\n }\n}\n\n/** 400 (see {@link ValidationError} for field validation failures). */\nexport class BadRequestError extends ApiError {\n override name = 'BadRequestError'\n}\n\n/**\n * 400 `ValidationError` — the request failed field validation. `validationErrors` is always an\n * array here (empty when the backend sent no field details).\n */\nexport class ValidationError extends BadRequestError {\n override name = 'ValidationError'\n declare public readonly validationErrors: FieldError[]\n\n constructor(\n message: string,\n errorClass?: ErrorClassName,\n status?: number,\n validationErrors: FieldError[] | null = null,\n options: ApiErrorOptions = {},\n ) {\n super(message, errorClass, status, validationErrors ?? [], options)\n }\n}\n\n/** 401 — missing or invalid access token. */\nexport class AuthenticationError extends ApiError {\n override name = 'AuthenticationError'\n}\n\n/** 402 — the account is out of credits or over a plan limit. */\nexport class PaymentRequiredError extends ApiError {\n override name = 'PaymentRequiredError'\n}\n\n/**\n * 403 — the user lacks the permission for this action, or the action is refused (e.g. out of\n * office hours, see `OutOfOfficeHoursErrorExtra`).\n */\nexport class PermissionDeniedError extends ApiError {\n override name = 'PermissionDeniedError'\n}\n\n/** 404 */\nexport class NotFoundError extends ApiError {\n override name = 'NotFoundError'\n}\n\n/** 409 */\nexport class ConflictError extends ApiError {\n override name = 'ConflictError'\n}\n\n/** 422 */\nexport class UnprocessableEntityError extends ApiError {\n override name = 'UnprocessableEntityError'\n}\n\n/** 429 */\nexport class RateLimitError extends ApiError {\n override name = 'RateLimitError'\n\n /**\n * Seconds to wait before retrying, or `null` when the response does not say.\n *\n * Reads the standard `Retry-After` header (delay seconds or HTTP date) first, then the\n * `X-RateLimit-Reset` header the Digisac backend sends (Unix time in seconds).\n */\n get retryAfter(): number | null {\n return (\n parseRetryAfter(this.headers?.get('Retry-After')) ??\n parseRateLimitReset(this.headers?.get('X-RateLimit-Reset'))\n )\n }\n}\n\nconst secondsUntil = (epochMs: number): number =>\n Math.max(0, Math.ceil((epochMs - Date.now()) / 1000))\n\n/** `Retry-After`: delay seconds or an HTTP date. */\nfunction parseRetryAfter(value: string | null | undefined): number | null {\n if (!value?.trim()) {\n return null\n }\n const seconds = Number(value)\n if (Number.isFinite(seconds)) {\n return Math.max(0, seconds)\n }\n const date = Date.parse(value)\n return Number.isNaN(date) ? null : secondsUntil(date)\n}\n\n/**\n * `X-RateLimit-Reset`: Unix time in seconds (what the Digisac backend sends). Small values are\n * read as delay seconds instead, the other convention in use for this header.\n */\nfunction parseRateLimitReset(value: string | null | undefined): number | null {\n if (!value?.trim()) {\n return null\n }\n const reset = Number(value)\n if (!Number.isFinite(reset)) {\n return null\n }\n // Anything before 2001-09-09 (1e9 s) can't be a reset time, so it is a delay.\n return reset < 1e9 ? Math.max(0, reset) : secondsUntil(reset * 1000)\n}\n\n/** 5xx */\nexport class InternalServerError extends ApiError {\n override name = 'InternalServerError'\n}\n\nfunction errorClassFor(status: number, errorClass: string | undefined): typeof ApiError {\n if (status === 400 && errorClass === 'ValidationError') {\n return ValidationError\n }\n\n switch (status) {\n case 400:\n return BadRequestError\n case 401:\n return AuthenticationError\n case 402:\n return PaymentRequiredError\n case 403:\n return PermissionDeniedError\n case 404:\n return NotFoundError\n case 409:\n return ConflictError\n case 422:\n return UnprocessableEntityError\n case 429:\n return RateLimitError\n default:\n return status >= 500 ? InternalServerError : ApiError\n }\n}\n"],"mappings":"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"}
@@ -83,7 +83,10 @@ export declare class AuthenticationError extends ApiError {
83
83
  export declare class PaymentRequiredError extends ApiError {
84
84
  name: string;
85
85
  }
86
- /** 403 — the user lacks the permission for this action. */
86
+ /**
87
+ * 403 — the user lacks the permission for this action, or the action is refused (e.g. out of
88
+ * office hours, see `OutOfOfficeHoursErrorExtra`).
89
+ */
87
90
  export declare class PermissionDeniedError extends ApiError {
88
91
  name: string;
89
92
  }
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"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"}
@@ -105,7 +105,10 @@ var AuthenticationError = class extends ApiError {
105
105
  var PaymentRequiredError = class extends ApiError {
106
106
  name = "PaymentRequiredError";
107
107
  };
108
- /** 403 — the user lacks the permission for this action. */
108
+ /**
109
+ * 403 — the user lacks the permission for this action, or the action is refused (e.g. out of
110
+ * office hours, see `OutOfOfficeHoursErrorExtra`).
111
+ */
109
112
  var PermissionDeniedError = class extends ApiError {
110
113
  name = "PermissionDeniedError";
111
114
  };
@@ -1 +1 @@
1
- {"version":3,"file":"errors.mjs","names":[],"sources":["../../src/core/errors.ts"],"sourcesContent":["/**\n * A field-level error from the backend `validate` middleware — one entry per failed rule.\n */\nexport interface FieldError {\n /** Where the field was read from: `body`, `params`, `query`, `headers` or `cookies`. */\n location: string\n /** The field name, e.g. `name`. */\n field: string\n /** `<location>.<field>`, e.g. `body.name`. */\n path: string\n message: string\n /** The failed rule, e.g. `required` or `hasLengthLesserThanOrEqual`. */\n code: string\n}\n\n/**\n * The backend error name sent as `error` in the body (`ApiError.errorClass`). Known names are listed\n * for autocomplete; any other string is still accepted. `HttpError` is the generic one: most 400s\n * and every unexpected 500.\n */\nexport type ErrorClassName =\n | 'HttpError'\n | 'ValidationError'\n | 'BadRequestHttpError'\n | 'UnauthorizedHttpError'\n | 'PaymentRequired'\n | 'ForbiddenHttpError'\n | 'NotFoundHttpError'\n | 'TooManyRequestsHttpError'\n | (string & {})\n\nexport interface ApiErrorOptions {\n /** Response headers. */\n headers?: Headers\n /** The parsed JSON error body, or the raw text when it is not JSON. */\n body?: unknown\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Flattens the backend `errors` object — `{ <location>: { <field>: { messages: string[], types: string[] } } }`\n * — into one {@link FieldError} per message. Returns `null` when there are no field errors.\n */\nexport function parseFieldErrors(errors: unknown): FieldError[] | null {\n if (!isRecord(errors)) {\n return null\n }\n\n const parsed: FieldError[] = []\n for (const [location, fields] of Object.entries(errors)) {\n if (!isRecord(fields)) {\n continue\n }\n for (const [field, detail] of Object.entries(fields)) {\n if (!isRecord(detail)) {\n continue\n }\n const messages = Array.isArray(detail.messages) ? detail.messages.map(String) : []\n const types = Array.isArray(detail.types) ? detail.types.map(String) : []\n messages.forEach((message, index) => {\n parsed.push({\n location,\n field,\n path: `${location}.${field}`,\n message,\n code: types[index] ?? types[0] ?? '',\n })\n })\n }\n }\n\n return parsed.length ? parsed : null\n}\n\n/** Base class of every error thrown by the SDK. */\nexport class DigisacError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'DigisacError'\n }\n}\n\n/** The request never got an HTTP response (network failure, DNS, connection refused, ...). */\nexport class ApiConnectionError extends DigisacError {\n constructor(message = 'Connection error.', options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'ApiConnectionError'\n }\n}\n\n/**\n * The API answered with a non-2xx status. Thrown as the subclass matching the status\n * (`NotFoundError`, `BadRequestError`, ...) — or `ValidationError` for a 400 field validation\n * failure — so callers can narrow with `instanceof`.\n */\nexport class ApiError extends DigisacError {\n /** The backend error name (`error` in the body), e.g. `ValidationError` or `PaymentRequired`. */\n public readonly errorClass?: ErrorClassName | undefined\n public readonly status?: number | undefined\n /** Field errors of a 400 `ValidationError`; `null` when the response carries none. */\n public readonly validationErrors: FieldError[] | null\n public readonly headers?: Headers | undefined\n /** The parsed JSON error body (`{ error, message, status, errors?, extra? }`), or the raw text. */\n public readonly body?: unknown\n\n constructor(\n message: string,\n errorClass?: ErrorClassName,\n status?: number,\n validationErrors: FieldError[] | null = null,\n options: ApiErrorOptions = {},\n ) {\n super(message)\n this.name = 'ApiError'\n this.errorClass = errorClass\n this.status = status\n this.validationErrors = validationErrors\n this.headers = options.headers\n this.body = options.body\n }\n\n /**\n * Builds the `ApiError` subclass for a response and its (already read) body: `ValidationError`\n * for a 400 `ValidationError`, else the class of the status.\n */\n static fromResponse(\n status: number,\n statusText: string,\n headers: Headers,\n body: unknown,\n ): ApiError {\n let message = `HTTP Error ${status}: ${statusText}`\n let errorClass: string | undefined\n let validationErrors: FieldError[] | null = null\n\n if (isRecord(body)) {\n const apiMessage = typeof body.message === 'string' ? body.message : undefined\n errorClass = typeof body.error === 'string' ? body.error : undefined\n message = apiMessage || errorClass || message\n validationErrors = parseFieldErrors(body.errors)\n }\n\n const ErrorClass = errorClassFor(status, errorClass)\n return new ErrorClass(message, errorClass, status, validationErrors, { headers, body })\n }\n}\n\n/** 400 (see {@link ValidationError} for field validation failures). */\nexport class BadRequestError extends ApiError {\n override name = 'BadRequestError'\n}\n\n/**\n * 400 `ValidationError` — the request failed field validation. `validationErrors` is always an\n * array here (empty when the backend sent no field details).\n */\nexport class ValidationError extends BadRequestError {\n override name = 'ValidationError'\n declare public readonly validationErrors: FieldError[]\n\n constructor(\n message: string,\n errorClass?: ErrorClassName,\n status?: number,\n validationErrors: FieldError[] | null = null,\n options: ApiErrorOptions = {},\n ) {\n super(message, errorClass, status, validationErrors ?? [], options)\n }\n}\n\n/** 401 — missing or invalid access token. */\nexport class AuthenticationError extends ApiError {\n override name = 'AuthenticationError'\n}\n\n/** 402 — the account is out of credits or over a plan limit. */\nexport class PaymentRequiredError extends ApiError {\n override name = 'PaymentRequiredError'\n}\n\n/** 403 — the user lacks the permission for this action. */\nexport class PermissionDeniedError extends ApiError {\n override name = 'PermissionDeniedError'\n}\n\n/** 404 */\nexport class NotFoundError extends ApiError {\n override name = 'NotFoundError'\n}\n\n/** 409 */\nexport class ConflictError extends ApiError {\n override name = 'ConflictError'\n}\n\n/** 422 */\nexport class UnprocessableEntityError extends ApiError {\n override name = 'UnprocessableEntityError'\n}\n\n/** 429 */\nexport class RateLimitError extends ApiError {\n override name = 'RateLimitError'\n\n /**\n * Seconds to wait before retrying, or `null` when the response does not say.\n *\n * Reads the standard `Retry-After` header (delay seconds or HTTP date) first, then the\n * `X-RateLimit-Reset` header the Digisac backend sends (Unix time in seconds).\n */\n get retryAfter(): number | null {\n return (\n parseRetryAfter(this.headers?.get('Retry-After')) ??\n parseRateLimitReset(this.headers?.get('X-RateLimit-Reset'))\n )\n }\n}\n\nconst secondsUntil = (epochMs: number): number =>\n Math.max(0, Math.ceil((epochMs - Date.now()) / 1000))\n\n/** `Retry-After`: delay seconds or an HTTP date. */\nfunction parseRetryAfter(value: string | null | undefined): number | null {\n if (!value?.trim()) {\n return null\n }\n const seconds = Number(value)\n if (Number.isFinite(seconds)) {\n return Math.max(0, seconds)\n }\n const date = Date.parse(value)\n return Number.isNaN(date) ? null : secondsUntil(date)\n}\n\n/**\n * `X-RateLimit-Reset`: Unix time in seconds (what the Digisac backend sends). Small values are\n * read as delay seconds instead, the other convention in use for this header.\n */\nfunction parseRateLimitReset(value: string | null | undefined): number | null {\n if (!value?.trim()) {\n return null\n }\n const reset = Number(value)\n if (!Number.isFinite(reset)) {\n return null\n }\n // Anything before 2001-09-09 (1e9 s) can't be a reset time, so it is a delay.\n return reset < 1e9 ? Math.max(0, reset) : secondsUntil(reset * 1000)\n}\n\n/** 5xx */\nexport class InternalServerError extends ApiError {\n override name = 'InternalServerError'\n}\n\nfunction errorClassFor(status: number, errorClass: string | undefined): typeof ApiError {\n if (status === 400 && errorClass === 'ValidationError') {\n return ValidationError\n }\n\n switch (status) {\n case 400:\n return BadRequestError\n case 401:\n return AuthenticationError\n case 402:\n return PaymentRequiredError\n case 403:\n return PermissionDeniedError\n case 404:\n return NotFoundError\n case 409:\n return ConflictError\n case 422:\n return UnprocessableEntityError\n case 429:\n return RateLimitError\n default:\n return status >= 500 ? InternalServerError : ApiError\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"errors.mjs","names":[],"sources":["../../src/core/errors.ts"],"sourcesContent":["/**\n * A field-level error from the backend `validate` middleware — one entry per failed rule.\n */\nexport interface FieldError {\n /** Where the field was read from: `body`, `params`, `query`, `headers` or `cookies`. */\n location: string\n /** The field name, e.g. `name`. */\n field: string\n /** `<location>.<field>`, e.g. `body.name`. */\n path: string\n message: string\n /** The failed rule, e.g. `required` or `hasLengthLesserThanOrEqual`. */\n code: string\n}\n\n/**\n * The backend error name sent as `error` in the body (`ApiError.errorClass`). Known names are listed\n * for autocomplete; any other string is still accepted. `HttpError` is the generic one: most 400s\n * and every unexpected 500.\n */\nexport type ErrorClassName =\n | 'HttpError'\n | 'ValidationError'\n | 'BadRequestHttpError'\n | 'UnauthorizedHttpError'\n | 'PaymentRequired'\n | 'ForbiddenHttpError'\n | 'NotFoundHttpError'\n | 'TooManyRequestsHttpError'\n | (string & {})\n\nexport interface ApiErrorOptions {\n /** Response headers. */\n headers?: Headers\n /** The parsed JSON error body, or the raw text when it is not JSON. */\n body?: unknown\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Flattens the backend `errors` object — `{ <location>: { <field>: { messages: string[], types: string[] } } }`\n * — into one {@link FieldError} per message. Returns `null` when there are no field errors.\n */\nexport function parseFieldErrors(errors: unknown): FieldError[] | null {\n if (!isRecord(errors)) {\n return null\n }\n\n const parsed: FieldError[] = []\n for (const [location, fields] of Object.entries(errors)) {\n if (!isRecord(fields)) {\n continue\n }\n for (const [field, detail] of Object.entries(fields)) {\n if (!isRecord(detail)) {\n continue\n }\n const messages = Array.isArray(detail.messages) ? detail.messages.map(String) : []\n const types = Array.isArray(detail.types) ? detail.types.map(String) : []\n messages.forEach((message, index) => {\n parsed.push({\n location,\n field,\n path: `${location}.${field}`,\n message,\n code: types[index] ?? types[0] ?? '',\n })\n })\n }\n }\n\n return parsed.length ? parsed : null\n}\n\n/** Base class of every error thrown by the SDK. */\nexport class DigisacError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'DigisacError'\n }\n}\n\n/** The request never got an HTTP response (network failure, DNS, connection refused, ...). */\nexport class ApiConnectionError extends DigisacError {\n constructor(message = 'Connection error.', options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'ApiConnectionError'\n }\n}\n\n/**\n * The API answered with a non-2xx status. Thrown as the subclass matching the status\n * (`NotFoundError`, `BadRequestError`, ...) — or `ValidationError` for a 400 field validation\n * failure — so callers can narrow with `instanceof`.\n */\nexport class ApiError extends DigisacError {\n /** The backend error name (`error` in the body), e.g. `ValidationError` or `PaymentRequired`. */\n public readonly errorClass?: ErrorClassName | undefined\n public readonly status?: number | undefined\n /** Field errors of a 400 `ValidationError`; `null` when the response carries none. */\n public readonly validationErrors: FieldError[] | null\n public readonly headers?: Headers | undefined\n /** The parsed JSON error body (`{ error, message, status, errors?, extra? }`), or the raw text. */\n public readonly body?: unknown\n\n constructor(\n message: string,\n errorClass?: ErrorClassName,\n status?: number,\n validationErrors: FieldError[] | null = null,\n options: ApiErrorOptions = {},\n ) {\n super(message)\n this.name = 'ApiError'\n this.errorClass = errorClass\n this.status = status\n this.validationErrors = validationErrors\n this.headers = options.headers\n this.body = options.body\n }\n\n /**\n * Builds the `ApiError` subclass for a response and its (already read) body: `ValidationError`\n * for a 400 `ValidationError`, else the class of the status.\n */\n static fromResponse(\n status: number,\n statusText: string,\n headers: Headers,\n body: unknown,\n ): ApiError {\n let message = `HTTP Error ${status}: ${statusText}`\n let errorClass: string | undefined\n let validationErrors: FieldError[] | null = null\n\n if (isRecord(body)) {\n const apiMessage = typeof body.message === 'string' ? body.message : undefined\n errorClass = typeof body.error === 'string' ? body.error : undefined\n message = apiMessage || errorClass || message\n validationErrors = parseFieldErrors(body.errors)\n }\n\n const ErrorClass = errorClassFor(status, errorClass)\n return new ErrorClass(message, errorClass, status, validationErrors, { headers, body })\n }\n}\n\n/** 400 (see {@link ValidationError} for field validation failures). */\nexport class BadRequestError extends ApiError {\n override name = 'BadRequestError'\n}\n\n/**\n * 400 `ValidationError` — the request failed field validation. `validationErrors` is always an\n * array here (empty when the backend sent no field details).\n */\nexport class ValidationError extends BadRequestError {\n override name = 'ValidationError'\n declare public readonly validationErrors: FieldError[]\n\n constructor(\n message: string,\n errorClass?: ErrorClassName,\n status?: number,\n validationErrors: FieldError[] | null = null,\n options: ApiErrorOptions = {},\n ) {\n super(message, errorClass, status, validationErrors ?? [], options)\n }\n}\n\n/** 401 — missing or invalid access token. */\nexport class AuthenticationError extends ApiError {\n override name = 'AuthenticationError'\n}\n\n/** 402 — the account is out of credits or over a plan limit. */\nexport class PaymentRequiredError extends ApiError {\n override name = 'PaymentRequiredError'\n}\n\n/**\n * 403 — the user lacks the permission for this action, or the action is refused (e.g. out of\n * office hours, see `OutOfOfficeHoursErrorExtra`).\n */\nexport class PermissionDeniedError extends ApiError {\n override name = 'PermissionDeniedError'\n}\n\n/** 404 */\nexport class NotFoundError extends ApiError {\n override name = 'NotFoundError'\n}\n\n/** 409 */\nexport class ConflictError extends ApiError {\n override name = 'ConflictError'\n}\n\n/** 422 */\nexport class UnprocessableEntityError extends ApiError {\n override name = 'UnprocessableEntityError'\n}\n\n/** 429 */\nexport class RateLimitError extends ApiError {\n override name = 'RateLimitError'\n\n /**\n * Seconds to wait before retrying, or `null` when the response does not say.\n *\n * Reads the standard `Retry-After` header (delay seconds or HTTP date) first, then the\n * `X-RateLimit-Reset` header the Digisac backend sends (Unix time in seconds).\n */\n get retryAfter(): number | null {\n return (\n parseRetryAfter(this.headers?.get('Retry-After')) ??\n parseRateLimitReset(this.headers?.get('X-RateLimit-Reset'))\n )\n }\n}\n\nconst secondsUntil = (epochMs: number): number =>\n Math.max(0, Math.ceil((epochMs - Date.now()) / 1000))\n\n/** `Retry-After`: delay seconds or an HTTP date. */\nfunction parseRetryAfter(value: string | null | undefined): number | null {\n if (!value?.trim()) {\n return null\n }\n const seconds = Number(value)\n if (Number.isFinite(seconds)) {\n return Math.max(0, seconds)\n }\n const date = Date.parse(value)\n return Number.isNaN(date) ? null : secondsUntil(date)\n}\n\n/**\n * `X-RateLimit-Reset`: Unix time in seconds (what the Digisac backend sends). Small values are\n * read as delay seconds instead, the other convention in use for this header.\n */\nfunction parseRateLimitReset(value: string | null | undefined): number | null {\n if (!value?.trim()) {\n return null\n }\n const reset = Number(value)\n if (!Number.isFinite(reset)) {\n return null\n }\n // Anything before 2001-09-09 (1e9 s) can't be a reset time, so it is a delay.\n return reset < 1e9 ? Math.max(0, reset) : secondsUntil(reset * 1000)\n}\n\n/** 5xx */\nexport class InternalServerError extends ApiError {\n override name = 'InternalServerError'\n}\n\nfunction errorClassFor(status: number, errorClass: string | undefined): typeof ApiError {\n if (status === 400 && errorClass === 'ValidationError') {\n return ValidationError\n }\n\n switch (status) {\n case 400:\n return BadRequestError\n case 401:\n return AuthenticationError\n case 402:\n return PaymentRequiredError\n case 403:\n return PermissionDeniedError\n case 404:\n return NotFoundError\n case 409:\n return ConflictError\n case 422:\n return UnprocessableEntityError\n case 429:\n return RateLimitError\n default:\n return status >= 500 ? InternalServerError : ApiError\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikatec/digisac-api-sdk",
3
- "version": "4.15.0",
3
+ "version": "4.16.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -23,8 +23,8 @@ export class AbsenceApi extends BaseCrudApi<Absence, CreateAbsencePayload, Updat
23
23
  }
24
24
 
25
25
  /**
26
- * The absence report of the account (`GET /absence`), always paginated, with its own query (the rows are
27
- * raw: see `AbsenceReportItem`). Prefer it to `getMany`, whose `where`/`include`/`paginate` the backend
26
+ * The absence report of the account (`GET /absence`), always paginated, with its own query (see
27
+ * `AbsenceReportItem`). Prefer it to `getMany`, whose `where`/`include`/`paginate` the backend
28
28
  * ignores. Requires both permissions, always: without them it answers 403
29
29
  * `MISSING_ABSENCE_REPORT_PERMISSION`.
30
30
  * @permissions overview.absence.view AND myAccount.absence
@@ -56,7 +56,7 @@ export type AbsenceReportQuery = {
56
56
  }
57
57
 
58
58
  /**
59
- * A row of the report: the stored absence with its user, both raw (not transformed).
59
+ * A row of the report: the stored absence with its `user`.
60
60
  */
61
61
  export type AbsenceReportItem = Omit<Absence, 'deletedAt' | 'user'> & {
62
62
  user?: Record<string, unknown>
@@ -64,4 +64,36 @@ describe('AnswersApi', () => {
64
64
  expect(await api.deleteById('a1', headers)).toBe('OK')
65
65
  expect(client.delete).toHaveBeenCalledWith('/answers/a1', headers)
66
66
  })
67
+
68
+ it('exportTxtRaw resolves the whole Response of requestRaw', async () => {
69
+ const response = new Response('Protocolo: 1\n', { headers: { 'Content-Type': 'plain/text' } })
70
+ const requestRaw = vi.fn().mockResolvedValue(response)
71
+ client.requestRaw = requestRaw
72
+ expect(await api.exportTxtRaw(query, headers)).toBe(response)
73
+ expect(requestRaw).toHaveBeenCalledWith(
74
+ 'GET',
75
+ `/answers/exportation-txt${encoded}`,
76
+ undefined,
77
+ headers,
78
+ )
79
+ })
80
+
81
+ it('exportCsvRaw calls requestRaw with /answers/exportation-csv', async () => {
82
+ const requestRaw = vi.fn().mockResolvedValue(new Response(''))
83
+ client.requestRaw = requestRaw
84
+ await api.exportCsvRaw(query, headers)
85
+ expect(requestRaw).toHaveBeenCalledWith(
86
+ 'GET',
87
+ `/answers/exportation-csv${encoded}`,
88
+ undefined,
89
+ headers,
90
+ )
91
+ })
92
+
93
+ it.each(['exportCsvRaw', 'exportTxtRaw'] as const)(
94
+ '%s refuses a client without requestRaw',
95
+ async (method) => {
96
+ await expect(api[method](query)).rejects.toThrow(`${method} needs a client with requestRaw`)
97
+ },
98
+ )
67
99
  })
@@ -10,8 +10,7 @@ import type {
10
10
  } from './types'
11
11
 
12
12
  /**
13
- * Answers of the evaluation questions. They are written by the bot flow (`SEND_QUESTION`); the
14
- * CRUD routes exist but validate nothing.
13
+ * Answers of the evaluation questions. They are written by the bot flow (`SEND_QUESTION`).
15
14
  *
16
15
  * Permissions: `evaluation.view` to read, `evaluation.create`/`update`/`destroy` to write.
17
16
  */
@@ -70,4 +69,41 @@ export class AnswersApi extends BaseCrudApi<
70
69
  headers,
71
70
  )
72
71
  }
72
+
73
+ /**
74
+ * The {@link exportCsv} export as the whole `Response`, to read its headers (`Content-Type`,
75
+ * `Content-Disposition`) as well as the body. Needs a client with `requestRaw`. Errors are thrown
76
+ * as in `exportCsv`.
77
+ * @permissions overview.evaluation.view
78
+ */
79
+ exportCsvRaw(query?: AnswersReportQuery, headers?: Record<string, string>): Promise<Response> {
80
+ return this.exportResponse('exportation-csv', 'exportCsvRaw', query, headers)
81
+ }
82
+
83
+ /**
84
+ * The {@link exportTxt} export as the whole `Response`, to read its headers (`Content-Type`,
85
+ * `Content-Disposition`) as well as the body. The Content-Type is `plain/text` (not
86
+ * `text/plain`). Needs a client with `requestRaw`. Errors are thrown as in `exportTxt`.
87
+ * @permissions overview.evaluation.view
88
+ */
89
+ exportTxtRaw(query?: AnswersReportQuery, headers?: Record<string, string>): Promise<Response> {
90
+ return this.exportResponse('exportation-txt', 'exportTxtRaw', query, headers)
91
+ }
92
+
93
+ private async exportResponse(
94
+ route: 'exportation-csv' | 'exportation-txt',
95
+ method: 'exportCsvRaw' | 'exportTxtRaw',
96
+ query?: AnswersReportQuery,
97
+ headers?: Record<string, string>,
98
+ ): Promise<Response> {
99
+ if (!this.client.requestRaw) {
100
+ throw new Error(`${method} needs a client with requestRaw (the answer is read as a Response)`)
101
+ }
102
+ return this.client.requestRaw(
103
+ 'GET',
104
+ `${this.basePath}/${route}${this.toQueryString(query)}`,
105
+ undefined,
106
+ headers,
107
+ )
108
+ }
73
109
  }
@@ -26,8 +26,7 @@ export type Answer = {
26
26
  export type AnswerRelationships = 'question' | 'ticket'
27
27
 
28
28
  /**
29
- * The route validates nothing: not the question, the ticket nor their account, and the answer is
30
- * saved even for an open ticket.
29
+ * The answer of a question of the account to a ticket of the account (an open ticket included).
31
30
  */
32
31
  export type CreateAnswerPayload = {
33
32
  text: string
@@ -174,4 +174,20 @@ describe('BotsApi', () => {
174
174
  expect(client.post).toHaveBeenCalledWith('/bots/suggest-agent-prompt', body, undefined)
175
175
  })
176
176
  })
177
+
178
+ describe('exportErrors', () => {
179
+ it('resolves the binary xlsx of requestRaw', async () => {
180
+ const requestRaw = vi.fn().mockResolvedValue(new Response(new ArrayBuffer(8)))
181
+ client.requestRaw = requestRaw
182
+ const result = await api.exportErrors('b1', headers)
183
+ expect(result.byteLength).toBe(8)
184
+ expect(requestRaw).toHaveBeenCalledWith('GET', '/bots/b1/errors/export', undefined, headers)
185
+ })
186
+
187
+ it('refuses a client without requestRaw', async () => {
188
+ await expect(api.exportErrors('b1')).rejects.toThrow(
189
+ 'exportErrors needs a client with requestRaw',
190
+ )
191
+ })
192
+ })
177
193
  })
@@ -21,9 +21,6 @@ import type {
21
21
  * Bots. The list route always paginates (`paginate: false` is ignored), so this class does not
22
22
  * extend `BaseCrudApi`. The contexts travel as a list (`BotContextInput` / `BotParsedContext`),
23
23
  * not as the stored map that `BotVersionsApi` serves.
24
- *
25
- * `GET /bots/:id/errors/export` is not exposed: it answers a binary xlsx, and `ApiClient` only
26
- * handles JSON/text bodies.
27
24
  */
28
25
  export class BotsApi
29
26
  extends BaseApi
@@ -107,10 +104,32 @@ export class BotsApi
107
104
  )
108
105
  }
109
106
 
107
+ /**
108
+ * Exports the failed steps of the bot runs of the last 30 days as an xlsx workbook (binary).
109
+ * Needs a client with `requestRaw`. A period without errors answers 400 `No errors found to
110
+ * export`; an unknown bot answers 404.
111
+ * @permissions bots.view
112
+ */
113
+ async exportErrors(id: string, headers?: Record<string, string>): Promise<ArrayBuffer> {
114
+ if (!this.client.requestRaw) {
115
+ throw new Error('exportErrors needs a client with requestRaw (the answer is a binary xlsx)')
116
+ }
117
+ const response = await this.client.requestRaw(
118
+ 'GET',
119
+ `${this.basePath}/${id}/errors/export`,
120
+ undefined,
121
+ headers,
122
+ )
123
+ return response.arrayBuffer()
124
+ }
125
+
110
126
  /**
111
127
  * Fires the `API_SIGNAL` trigger of the bot for the contact, with an optional `flag` (read by
112
128
  * the `api_signal_flag` condition). Asynchronous: always resolves with no content, and the
113
- * worker only runs it when the contact has an open ticket.
129
+ * worker only runs it when the contact has an open ticket. The worker runs the flow of `botId`
130
+ * on the engine of the bot of the contact's connection (see `BotSettings.botCreatedVersion`; when
131
+ * the connection has no bot, the engine of the `botId` bot). `triggerSignal` does not reach the
132
+ * bot simulator (there the `API_SIGNAL` path is picked by hand, with no `flag`).
114
133
  * @permissions bots.view
115
134
  */
116
135
  triggerSignal(
@@ -148,9 +167,8 @@ export class BotsApi
148
167
 
149
168
  /**
150
169
  * Generates a prompt for an AI agent of the bot. COSTS one AI agent credit of the account and
151
- * calls OpenAI. No permission check: any authenticated user. A missing field answers 400; an
152
- * option outside the lists, or the AI agent limit of the account reached
153
- * (`Agent limit reached`), answers 500.
170
+ * calls OpenAI. A missing field answers 400; an option outside the lists, or the AI agent limit
171
+ * of the account reached (`Agent limit reached`), answers 500.
154
172
  */
155
173
  suggestAgentPrompt(
156
174
  body: SuggestAgentPromptPayload,
@@ -14,7 +14,11 @@ export type BotLimitations = {
14
14
  export type BotSettings = {
15
15
  /** If true, only the first matching rule per trigger fires; otherwise all matching rules fire. */
16
16
  sequentialRuleExecution?: boolean
17
- /** Version of the bot builder used to create this bot. v3 uses the visual flow builder. */
17
+ /**
18
+ * Version of the bot builder used to create this bot, which also picks the engine that runs it:
19
+ * `v3` (the visual flow builder) runs on the flow engine; `v1`, `v2` or absent, on the legacy
20
+ * engine. The bot simulator always runs the flow engine.
21
+ */
18
22
  botCreatedVersion?: BotCreatedVersion
19
23
  limitations?: BotLimitations
20
24
  }
@@ -191,6 +195,11 @@ export type BotActionDataMap = {
191
195
  /** Waits `waitSeconds` to group the contact's messages before answering. */
192
196
  messageGrouping?: { enabled?: boolean; waitSeconds?: number }
193
197
  }
198
+ /**
199
+ * Runs on the flow engine (`botCreatedVersion: 'v3'`). `endpointUrl`, `httpMethod`, `retries`,
200
+ * `timeout` and `bodyType` are required to run it: without one of them the step does not run.
201
+ * The bot simulator does not run it.
202
+ */
194
203
  HTTP_REQUEST: {
195
204
  requestName?: string
196
205
  endpointUrl?: string
@@ -204,8 +213,9 @@ export type BotActionDataMap = {
204
213
  requestBodyFormData?: { key: string; value: string }[]
205
214
  requestBodyJson?: string
206
215
  requestBodyXml?: string
216
+ /** Number of attempts (at least 1), not of retries after the first one. */
207
217
  retries?: number
208
- /** Timeout in milliseconds. */
218
+ /** Timeout of each attempt, in SECONDS. */
209
219
  timeout?: number
210
220
  /** Response properties to extract into bot session variables. */
211
221
  variables?: { key: string; value: string }[]
@@ -105,6 +105,54 @@ describe('CampaignsApi', () => {
105
105
  })
106
106
  })
107
107
 
108
+ // ── getOne ───────────────────────────────────────────────────────
109
+
110
+ describe('getOne', () => {
111
+ it('reads the first page of one campaign, leaving out paginate and limit', async () => {
112
+ vi.mocked(client.get).mockResolvedValue(paginated)
113
+ const result = await api.getOne({
114
+ where: { title: 'Test Campaign' },
115
+ status: 'ready',
116
+ paginate: false,
117
+ limit: 5,
118
+ page: 3,
119
+ })
120
+ const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
121
+ expect(url.startsWith('/campaigns?query=')).toBe(true)
122
+ expect(JSON.parse(decodeURIComponent(url.slice('/campaigns?query='.length)))).toEqual({
123
+ where: { title: 'Test Campaign' },
124
+ status: 'ready',
125
+ page: 1,
126
+ perPage: 1,
127
+ })
128
+ expect(result).toEqual(campaign)
129
+ })
130
+
131
+ it('resolves null when no campaign matches', async () => {
132
+ vi.mocked(client.get).mockResolvedValue({ ...paginated, data: [], total: 0 })
133
+ expect(await api.getOne({ q: 'none' })).toBeNull()
134
+ })
135
+
136
+ it('resolves null for an empty body', async () => {
137
+ vi.mocked(client.get).mockResolvedValue(undefined)
138
+ expect(await api.getOne()).toBeNull()
139
+ expect(client.get).toHaveBeenCalledWith(
140
+ `/campaigns?query=${encodeURIComponent(JSON.stringify({ page: 1, perPage: 1 }))}`,
141
+ undefined,
142
+ )
143
+ })
144
+
145
+ it('forwards custom headers', async () => {
146
+ vi.mocked(client.get).mockResolvedValue(paginated)
147
+ const headers = { 'X-Custom': 'value' }
148
+ await api.getOne(undefined, headers)
149
+ expect(client.get).toHaveBeenCalledWith(
150
+ `/campaigns?query=${encodeURIComponent(JSON.stringify({ page: 1, perPage: 1 }))}`,
151
+ headers,
152
+ )
153
+ })
154
+ })
155
+
108
156
  // ── getById ──────────────────────────────────────────────────────
109
157
 
110
158
  describe('getById', () => {
@@ -1,5 +1,5 @@
1
1
  import qs from 'qs'
2
- import type { ApiClient } from '../../core'
2
+ import type { ApiClient, Paginated } from '../../core'
3
3
  import { BaseCrudApi } from '../../core'
4
4
  import type {
5
5
  AutoPauseModePayload,
@@ -25,6 +25,7 @@ import type {
25
25
  * {@link CampaignListQuery}); `getById` of an unknown id resolves `null`. `deleteById` answers the
26
26
  * text `'OK'`. `getMany({ paginate: false })` answers 400 `FILTER_PARAM_INVALID` with the default
27
27
  * `'json'` query format (the route only takes `paginate` as a string): use `withQueryFormat('qs')`.
28
+ * `getOne` reads a page of one campaign, so it works with every query format.
28
29
  */
29
30
  export class CampaignsApi extends BaseCrudApi<
30
31
  Campaign,
@@ -37,6 +38,28 @@ export class CampaignsApi extends BaseCrudApi<
37
38
  super(client, '/campaigns')
38
39
  }
39
40
 
41
+ /**
42
+ * Returns the first campaign matching the query, or `null` when none match. Reads the first page
43
+ * with one campaign (`page: 1`, `perPage: 1`: the `page` and `perPage` of the query are
44
+ * replaced, and its `paginate` and `limit` are left out), so the campaign comes with the `stats`
45
+ * of `getMany`.
46
+ * @permissions campaigns.view
47
+ */
48
+ override async getOne(
49
+ query?: CampaignListQuery,
50
+ headers?: Record<string, string>,
51
+ ): Promise<Campaign | null> {
52
+ const pageQuery: CampaignListQuery = { ...query, page: 1, perPage: 1 }
53
+ delete pageQuery.paginate
54
+ delete pageQuery.limit
55
+ const page = await this.client.get<Paginated<Campaign> | undefined>(
56
+ `${this.basePath}${this.toQueryString(pageQuery)}`,
57
+ headers,
58
+ )
59
+ // An empty response body resolves to `undefined`.
60
+ return page?.data?.[0] ?? null
61
+ }
62
+
40
63
  /**
41
64
  * Starts sending the campaign: answers it as `processing` (an `import_error` campaign goes back
42
65
  * to `importing_contacts`). It becomes `done` once every contact is fired.
@@ -205,7 +228,13 @@ export class CampaignsApi extends BaseCrudApi<
205
228
  )
206
229
  }
207
230
 
208
- /** Returns the HSM message usage and limit of an account. */
231
+ /**
232
+ * Returns the HSM message usage and limit of the account of the current user (send its
233
+ * `accountId`).
234
+ * @deprecated Use `WhatsappBusinessTemplatesApi.getHsmLimit` (the same route, outside
235
+ * `/campaigns`).
236
+ * @permissions hsm.view
237
+ */
209
238
  getHsmLimit(accountId: string, headers?: Record<string, string>): Promise<HsmLimit> {
210
239
  return this.client.get<HsmLimit>(`/whatsapp-business-templates/${accountId}/hsmLimit`, headers)
211
240
  }