@api-client/core 0.19.15 → 0.19.17

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@api-client/core",
3
3
  "description": "The API Client's core client library. Works in NodeJS and in a ES enabled browser.",
4
- "version": "0.19.15",
4
+ "version": "0.19.17",
5
5
  "license": "UNLICENSED",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -92,7 +92,7 @@
92
92
  "@pawel-up/csv": "^0.2.0",
93
93
  "@pawel-up/data-mock": "^0.4.0",
94
94
  "@pawel-up/jexl": "^4.0.1",
95
- "@xmldom/xmldom": "^0.8.11",
95
+ "@xmldom/xmldom": "^0.9.9",
96
96
  "chalk": "^5.4.1",
97
97
  "console-table-printer": "^2.11.2",
98
98
  "dompurify": "^3.2.6",
@@ -142,7 +142,7 @@
142
142
  "schema-org-json-schemas": "^2.1.4",
143
143
  "sinon": "^21.0.0",
144
144
  "ts-node-maintained": "^10.9.5",
145
- "typescript": "^5.5.2",
145
+ "typescript": "^6.0.2",
146
146
  "typescript-eslint": "^8.24.1"
147
147
  },
148
148
  "scripts": {
@@ -150,8 +150,7 @@
150
150
  "build:ts": "tsc --project tsconfig.json",
151
151
  "build:node": "tsc --project tsconfig.node.json",
152
152
  "build": "npm run build:ts && npm run copy:assets",
153
- "prepare": "husky && npm run fixes && npm run build:ts",
154
- "fixes": "node scripts/fix-rollup-plugin.js",
153
+ "prepare": "husky && npm run build:ts",
155
154
  "tsc": "tsc",
156
155
  "tsc:tests": "tsc --project tsconfig.browser.json",
157
156
  "tsc:watch": "tsc --watch --project tsconfig.json",
@@ -160,6 +159,7 @@
160
159
  "test": "npm run test:node && npm run test:browser",
161
160
  "test:coverage": "npm run test:node:coverage && npm run test:browser",
162
161
  "test:node": "node --import ts-node-maintained/register/esm --enable-source-maps bin/test.ts",
162
+ "test:node:watch": "node --import ts-node-maintained/register/esm --enable-source-maps --watch bin/test.ts",
163
163
  "test:node:coverage": "c8 --reporter lcov --reporter text node --import ts-node-maintained/register/esm --enable-source-maps bin/test.ts",
164
164
  "copy:assets": "cp -f ./oauth-popup.html ./build/oauth-popup.html",
165
165
  "start": "echo \"Use the npm run dev instead\"",
@@ -100,15 +100,16 @@ export class XmlReader extends DataReader {
100
100
  xResult = xpath.default.XPathResult
101
101
  let errored = false
102
102
  const parser = new DOMParser({
103
- errorHandler: (): void => {
103
+ onError: (level, msg): void => {
104
+ console.error(`[${level}] ${msg}`)
104
105
  errored = true
105
106
  },
106
107
  })
107
108
  let dom: Document | undefined
108
109
  try {
109
110
  dom = parser.parseFromString(data, 'text/xml') as unknown as Document
110
- } catch {
111
- //
111
+ } catch (e) {
112
+ console.error(e)
112
113
  }
113
114
  if (!dom || errored) {
114
115
  return undefined
@@ -1,3 +1,8 @@
1
+ /**
2
+ * A serialized representation of an Exception.
3
+ * Provides a standardized structure for pure data objects,
4
+ * typically used when converting errors for JSON responses.
5
+ */
1
6
  export interface ExceptionSchema {
2
7
  name: string
3
8
  message: string
@@ -75,39 +80,49 @@ export class Exception extends Error {
75
80
  */
76
81
  static fromRawException(init: object, defaultMessage: string): Exception {
77
82
  const typed = init as Record<string, string | number>
78
- const options: ErrorOptions & { code?: string; status?: number } = {}
79
- if (typed.code) {
80
- options.code = typed.code as string
83
+ const options: ErrorOptions & { code?: string; status?: number; help?: string } = {}
84
+ if (typed.code !== undefined) {
85
+ options.code = String(typed.code)
81
86
  }
82
- if (typed.status) {
83
- options.status = Number(typed.status)
87
+ if (typed.status !== undefined) {
88
+ const parsedStatus = Number(typed.status)
89
+ if (!isNaN(parsedStatus)) {
90
+ options.status = parsedStatus
91
+ }
84
92
  }
85
- const message = typed.message ?? defaultMessage
86
- const result = new this(message as string, options)
87
- if (typed.help) {
88
- result.setHelp(typed.help as string)
93
+ if (typed.help !== undefined) {
94
+ options.help = String(typed.help)
89
95
  }
90
- if (typed.name) {
91
- result.name = typed.name as string
96
+ const message = typed.message !== undefined ? String(typed.message) : defaultMessage
97
+ const result = new this(message, options)
98
+ if (typed.name !== undefined) {
99
+ result.name = String(typed.name)
92
100
  }
93
101
  return result
94
102
  }
95
103
 
104
+ /**
105
+ * Initializes a new Exception instance.
106
+ *
107
+ * @param message - The primary human-readable error message.
108
+ * @param options - Additional properties configuration. Accepts standard `ErrorOptions`
109
+ * (like `cause`), plus custom `code`, `status`, and `help`.
110
+ */
96
111
  constructor(message?: string, options?: ErrorOptions & { code?: string; status?: number; help?: string }) {
97
112
  super(message, options)
98
113
 
99
114
  const ErrorConstructor = this.constructor as typeof Exception
100
115
 
101
116
  this.name = ErrorConstructor.name
102
- this.message = message || ErrorConstructor.message || ''
103
- this.status = options?.status || ErrorConstructor.status || 500
117
+ this.message = message ?? ErrorConstructor.message ?? ''
118
+ this.status = options?.status ?? ErrorConstructor.status ?? 500
104
119
 
105
- const code = options?.code || ErrorConstructor.code
120
+ const code = options?.code ?? ErrorConstructor.code
106
121
  if (code !== undefined) {
107
122
  this.code = code
108
123
  }
109
124
 
110
- const help = options?.help || ErrorConstructor.help
125
+ const help = options?.help ?? ErrorConstructor.help
111
126
  if (help !== undefined) {
112
127
  this.help = help
113
128
  }
@@ -115,25 +130,53 @@ export class Exception extends Error {
115
130
  Error.captureStackTrace(this, ErrorConstructor)
116
131
  }
117
132
 
133
+ /**
134
+ * Assigns a human-readable help description for troubleshooting and returns
135
+ * the exception instance to allow for method chaining.
136
+ *
137
+ * @param help - The detailed help informational text.
138
+ * @returns The current exception instance.
139
+ */
118
140
  setHelp(help: string): this {
119
141
  this.help = help
120
142
  return this
121
143
  }
122
144
 
145
+ /**
146
+ * Assigns a programmatic error code and returns the exception instance
147
+ * to allow for method chaining.
148
+ *
149
+ * @param code - The string error code (e.g., 'E_FILE_NOT_FOUND').
150
+ * @returns The current exception instance.
151
+ */
123
152
  setCode(code: string): this {
124
153
  this.code = code
125
154
  return this
126
155
  }
127
156
 
157
+ /**
158
+ * Assigns an HTTP-style status code and returns the exception instance
159
+ * to allow for method chaining.
160
+ *
161
+ * @param status - The numeric status code (e.g., 404, 500).
162
+ * @returns The current exception instance.
163
+ */
128
164
  setStatus(status: number): this {
129
165
  this.status = status
130
166
  return this
131
167
  }
132
168
 
169
+ /**
170
+ * Provides the string tag used by `Object.prototype.toString`.
171
+ */
133
172
  get [Symbol.toStringTag]() {
134
173
  return this.constructor.name
135
174
  }
136
175
 
176
+ /**
177
+ * Computes a string representation of the exception, including the
178
+ * programmatic code if one is defined.
179
+ */
137
180
  override toString(): string {
138
181
  if (this.code) {
139
182
  return `${this.name} [${this.code}]: ${this.message}`
@@ -141,6 +184,12 @@ export class Exception extends Error {
141
184
  return `${this.name}: ${this.message}`
142
185
  }
143
186
 
187
+ /**
188
+ * Serializes the exception into a plain JavaScript object.
189
+ * This ensures safe passing across boundaries and correct format in JSON responses.
190
+ *
191
+ * @returns The serialized exception object matching `ExceptionSchema`.
192
+ */
144
193
  toJSON(): ExceptionSchema {
145
194
  const result: ExceptionSchema = {
146
195
  name: this.name,
@@ -158,3 +207,58 @@ export class Exception extends Error {
158
207
  return result
159
208
  }
160
209
  }
210
+
211
+ /**
212
+ * Converts a standard Error object (or thrown primitive) into an Exception instance.
213
+ * This function extracts relevant properties from the raw error
214
+ * and sets them on the Exception instance, allowing for a more
215
+ * consistent error handling experience across the application.
216
+ * @param error The unknown error value to convert.
217
+ * @param defaults Optional default properties to apply if they are missing on the Error object.
218
+ * @returns An Exception instance with properties set from the Error object.
219
+ */
220
+ export function fromError(error: unknown, defaults: Partial<ExceptionSchema> = {}): Exception {
221
+ let message: string | undefined
222
+ let typed: Record<string, string | number> = {}
223
+
224
+ if (error instanceof Error) {
225
+ message = error.message
226
+ typed = error as unknown as Record<string, string | number>
227
+ } else if (typeof error === 'string') {
228
+ message = error
229
+ } else if (error !== null && typeof error === 'object') {
230
+ typed = error as Record<string, string | number>
231
+ if (typeof typed.message === 'string') {
232
+ message = typed.message
233
+ }
234
+ }
235
+
236
+ const e = new Exception(message || defaults.message || 'An error occurred')
237
+
238
+ if (typed.code !== undefined) {
239
+ e.code = String(typed.code)
240
+ } else if (defaults.code !== undefined) {
241
+ e.code = defaults.code
242
+ }
243
+
244
+ if (typed.status !== undefined) {
245
+ const status = Number(typed.status)
246
+ e.status = isNaN(status) ? (defaults.status ?? 500) : status
247
+ } else if (defaults.status !== undefined) {
248
+ e.status = defaults.status
249
+ }
250
+
251
+ if (typed.help !== undefined) {
252
+ e.setHelp(String(typed.help))
253
+ } else if (defaults.help !== undefined) {
254
+ e.setHelp(defaults.help)
255
+ }
256
+
257
+ if (typed.name !== undefined) {
258
+ e.name = String(typed.name)
259
+ } else if (defaults.name !== undefined) {
260
+ e.name = defaults.name
261
+ }
262
+
263
+ return e
264
+ }
@@ -0,0 +1,261 @@
1
+ import { test } from '@japa/runner'
2
+ import { Exception, fromError } from '../../../src/exceptions/exception.js'
3
+
4
+ test.group('Exceptions > Exception', () => {
5
+ test('constructor sets basic properties', ({ assert }) => {
6
+ const ex = new Exception('Test message', { code: 'E_TEST', status: 400, help: 'Help me' })
7
+ assert.equal(ex.message, 'Test message')
8
+ assert.equal(ex.code, 'E_TEST')
9
+ assert.equal(ex.status, 400)
10
+ assert.equal(ex.help, 'Help me')
11
+ assert.equal(ex.name, 'Exception')
12
+ })
13
+
14
+ test('constructor uses static defaults', ({ assert }) => {
15
+ class CustomException extends Exception {
16
+ static override message = 'Static message'
17
+ static override code = 'E_STATIC'
18
+ static override status = 404
19
+ static override help = 'Static help'
20
+ }
21
+ const ex = new CustomException()
22
+ assert.equal(ex.message, 'Static message')
23
+ assert.equal(ex.code, 'E_STATIC')
24
+ assert.equal(ex.status, 404)
25
+ assert.equal(ex.help, 'Static help')
26
+ assert.equal(ex.name, 'CustomException')
27
+ })
28
+
29
+ test('constructor defaults status to 500', ({ assert }) => {
30
+ const ex = new Exception('Test')
31
+ assert.equal(ex.status, 500)
32
+ })
33
+
34
+ test('constructor accepts status 0', ({ assert }) => {
35
+ const ex = new Exception('Test', { status: 0 })
36
+ assert.equal(ex.status, 0)
37
+ })
38
+
39
+ test('setters allow chaining and update properties', ({ assert }) => {
40
+ const ex = new Exception('Test')
41
+ const result = ex.setCode('E_CHAIN').setStatus(401).setHelp('Chained')
42
+
43
+ assert.strictEqual(result, ex)
44
+ assert.equal(ex.code, 'E_CHAIN')
45
+ assert.equal(ex.status, 401)
46
+ assert.equal(ex.help, 'Chained')
47
+ })
48
+
49
+ test('toString returns properly formatted string', ({ assert }) => {
50
+ const ex1 = new Exception('Error without code')
51
+ assert.equal(ex1.toString(), 'Exception: Error without code')
52
+
53
+ const ex2 = new Exception('Error with code', { code: 'E_CODE' })
54
+ assert.equal(ex2.toString(), 'Exception [E_CODE]: Error with code')
55
+ })
56
+
57
+ test('toStringTag returns class name', ({ assert }) => {
58
+ const ex = new Exception('Test')
59
+ assert.equal(Object.prototype.toString.call(ex), '[object Exception]')
60
+
61
+ class CustomTagError extends Exception {}
62
+ const ex2 = new CustomTagError('Test')
63
+ assert.equal(Object.prototype.toString.call(ex2), '[object CustomTagError]')
64
+ })
65
+
66
+ test('toJSON serializes properties', ({ assert }) => {
67
+ const ex = new Exception('Test message', { code: 'E_TEST', status: 400, help: 'Help me' })
68
+ const json = ex.toJSON()
69
+
70
+ assert.deepEqual(json, {
71
+ name: 'Exception',
72
+ message: 'Test message',
73
+ code: 'E_TEST',
74
+ status: 400,
75
+ help: 'Help me',
76
+ })
77
+ })
78
+
79
+ test('toJSON does not output unset optional properties', ({ assert }) => {
80
+ const ex = new Exception('Basic')
81
+ const json = ex.toJSON()
82
+
83
+ assert.deepEqual(json, {
84
+ name: 'Exception',
85
+ message: 'Basic',
86
+ status: 500,
87
+ })
88
+ })
89
+
90
+ test('fromRawException reconstructs exception', ({ assert }) => {
91
+ const raw = {
92
+ message: 'Raw message',
93
+ code: 'E_RAW',
94
+ status: '403',
95
+ help: 'Raw help',
96
+ name: 'RawName',
97
+ }
98
+ const ex = Exception.fromRawException(raw, 'Default')
99
+
100
+ assert.instanceOf(ex, Exception)
101
+ assert.equal(ex.message, 'Raw message')
102
+ assert.equal(ex.code, 'E_RAW')
103
+ assert.equal(ex.status, 403)
104
+ assert.equal(ex.help, 'Raw help')
105
+ assert.equal(ex.name, 'RawName')
106
+ })
107
+
108
+ test('fromRawException uses default message if missing', ({ assert }) => {
109
+ const ex = Exception.fromRawException({}, 'Fallback message')
110
+ assert.equal(ex.message, 'Fallback message')
111
+ })
112
+ })
113
+
114
+ test.group('Exceptions > Exception > fromError()', () => {
115
+ test('creates Exception from standard Error', ({ assert }) => {
116
+ const err = new Error('standard error message')
117
+ const ex = fromError(err)
118
+
119
+ assert.instanceOf(ex, Exception)
120
+ assert.equal(ex.message, 'standard error message')
121
+ })
122
+
123
+ test('creates Exception from Error with no message', ({ assert }) => {
124
+ const err = new Error()
125
+ const ex = fromError(err)
126
+
127
+ assert.instanceOf(ex, Exception)
128
+ assert.equal(ex.message, 'An error occurred')
129
+ })
130
+
131
+ test('copies code property', ({ assert }) => {
132
+ const err = new Error('test') as unknown as Exception
133
+ err.code = 'TEST_CODE'
134
+ const ex = fromError(err)
135
+
136
+ assert.equal(ex.code, 'TEST_CODE')
137
+ })
138
+
139
+ test('copies status property', ({ assert }) => {
140
+ const err = new Error('test') as unknown as Exception
141
+ err.status = 404
142
+ const ex = fromError(err)
143
+
144
+ assert.equal(ex.status, 404)
145
+ })
146
+
147
+ test('copies help property', ({ assert }) => {
148
+ const err = new Error('test') as unknown as Exception
149
+ err.help = 'Help text'
150
+ const ex = fromError(err)
151
+
152
+ assert.equal(ex.help, 'Help text')
153
+ })
154
+
155
+ test('copies name property', ({ assert }) => {
156
+ const err = new Error('test')
157
+ err.name = 'CustomErrorName'
158
+ const ex = fromError(err)
159
+
160
+ assert.equal(ex.name, 'CustomErrorName')
161
+ })
162
+
163
+ test('handles when object is already an Exception instance', ({ assert }) => {
164
+ const originalEx = new Exception('Original exception')
165
+ originalEx.code = 'ORIGINAL_CODE'
166
+ originalEx.status = 400
167
+ originalEx.setHelp('Original help')
168
+
169
+ const newEx = fromError(originalEx)
170
+
171
+ assert.instanceOf(newEx, Exception)
172
+ assert.equal(newEx.message, 'Original exception')
173
+ assert.equal(newEx.code, 'ORIGINAL_CODE')
174
+ assert.equal(newEx.status, 400)
175
+ assert.equal(newEx.help, 'Original help')
176
+ assert.equal(newEx.name, 'Exception')
177
+ })
178
+
179
+ test('uses default message when error has no message', ({ assert }) => {
180
+ const err = new Error()
181
+ err.message = ''
182
+ const ex = fromError(err, { message: 'Default message' })
183
+
184
+ assert.equal(ex.message, 'Default message')
185
+ })
186
+
187
+ test('error message takes precedence over default message', ({ assert }) => {
188
+ const err = new Error('Original message')
189
+ const ex = fromError(err, { message: 'Default message' })
190
+
191
+ assert.equal(ex.message, 'Original message')
192
+ })
193
+
194
+ test('uses default code when error has no code', ({ assert }) => {
195
+ const err = new Error('test')
196
+ const ex = fromError(err, { code: 'DEFAULT_CODE' })
197
+
198
+ assert.equal(ex.code, 'DEFAULT_CODE')
199
+ })
200
+
201
+ test('error code takes precedence over default code', ({ assert }) => {
202
+ const err = new Error('test') as unknown as Exception
203
+ err.code = 'ORIGINAL_CODE'
204
+ const ex = fromError(err, { code: 'DEFAULT_CODE' })
205
+
206
+ assert.equal(ex.code, 'ORIGINAL_CODE')
207
+ })
208
+
209
+ test('uses default status when error has no status', ({ assert }) => {
210
+ const err = new Error('test')
211
+ const ex = fromError(err, { status: 401 })
212
+
213
+ assert.equal(ex.status, 401)
214
+ })
215
+
216
+ test('uses default status when error status is invalid', ({ assert }) => {
217
+ const err = new Error('test') as unknown as Exception
218
+ err.status = 'invalid' as unknown as number
219
+ const ex = fromError(err, { status: 403 })
220
+
221
+ assert.equal(ex.status, 403)
222
+ })
223
+
224
+ test('error status takes precedence over default status', ({ assert }) => {
225
+ const err = new Error('test') as unknown as Exception
226
+ err.status = 404
227
+ const ex = fromError(err, { status: 401 })
228
+
229
+ assert.equal(ex.status, 404)
230
+ })
231
+
232
+ test('uses default help when error has no help', ({ assert }) => {
233
+ const err = new Error('test')
234
+ const ex = fromError(err, { help: 'Default help text' })
235
+
236
+ assert.equal(ex.help, 'Default help text')
237
+ })
238
+
239
+ test('error help takes precedence over default help', ({ assert }) => {
240
+ const err = new Error('test') as unknown as Exception
241
+ err.help = 'Original help text'
242
+ const ex = fromError(err, { help: 'Default help text' })
243
+
244
+ assert.equal(ex.help, 'Original help text')
245
+ })
246
+
247
+ test('uses default name when error has no name', ({ assert }) => {
248
+ const err = { message: 'test' } as unknown as Error
249
+ const ex = fromError(err, { name: 'DefaultName' })
250
+
251
+ assert.equal(ex.name, 'DefaultName')
252
+ })
253
+
254
+ test('error name takes precedence over default name', ({ assert }) => {
255
+ const err = new Error('test')
256
+ err.name = 'OriginalName'
257
+ const ex = fromError(err, { name: 'DefaultName' })
258
+
259
+ assert.equal(ex.name, 'OriginalName')
260
+ })
261
+ })