@datocms/rest-client-utils 5.2.8 → 5.7.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 (54) hide show
  1. package/dist/cjs/__tests__/buildNormalizedParams.test.js +11 -11
  2. package/dist/cjs/__tests__/buildNormalizedParams.test.js.map +1 -1
  3. package/dist/cjs/__tests__/errors.test.js +121 -0
  4. package/dist/cjs/__tests__/errors.test.js.map +1 -0
  5. package/dist/cjs/errors.js +55 -1
  6. package/dist/cjs/errors.js.map +1 -1
  7. package/dist/cjs/index.js +10 -10
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/makeCancelablePromise.js +28 -0
  10. package/dist/cjs/makeCancelablePromise.js.map +1 -1
  11. package/dist/cjs/pollJobResult.js +4 -4
  12. package/dist/cjs/pollJobResult.js.map +1 -1
  13. package/dist/cjs/rawPageIterator.js +2 -2
  14. package/dist/cjs/rawPageIterator.js.map +1 -1
  15. package/dist/cjs/request.js +14 -14
  16. package/dist/cjs/request.js.map +1 -1
  17. package/dist/esm/__tests__/buildNormalizedParams.test.js +1 -1
  18. package/dist/esm/__tests__/buildNormalizedParams.test.js.map +1 -1
  19. package/dist/esm/__tests__/errors.test.d.ts +1 -0
  20. package/dist/esm/__tests__/errors.test.js +119 -0
  21. package/dist/esm/__tests__/errors.test.js.map +1 -0
  22. package/dist/esm/errors.d.ts +16 -0
  23. package/dist/esm/errors.js +55 -1
  24. package/dist/esm/errors.js.map +1 -1
  25. package/dist/esm/index.d.ts +10 -10
  26. package/dist/esm/index.js +10 -10
  27. package/dist/esm/index.js.map +1 -1
  28. package/dist/esm/makeCancelablePromise.d.ts +9 -0
  29. package/dist/esm/makeCancelablePromise.js +28 -0
  30. package/dist/esm/makeCancelablePromise.js.map +1 -1
  31. package/dist/esm/package.json +1 -0
  32. package/dist/esm/pollJobResult.d.ts +1 -1
  33. package/dist/esm/pollJobResult.js +2 -2
  34. package/dist/esm/pollJobResult.js.map +1 -1
  35. package/dist/esm/rawPageIterator.js +1 -1
  36. package/dist/esm/rawPageIterator.js.map +1 -1
  37. package/dist/esm/request.d.ts +1 -1
  38. package/dist/esm/request.js +4 -4
  39. package/dist/esm/request.js.map +1 -1
  40. package/dist/types/__tests__/errors.test.d.ts +1 -0
  41. package/dist/types/errors.d.ts +16 -0
  42. package/dist/types/index.d.ts +10 -10
  43. package/dist/types/makeCancelablePromise.d.ts +9 -0
  44. package/dist/types/pollJobResult.d.ts +1 -1
  45. package/dist/types/request.d.ts +1 -1
  46. package/package.json +13 -3
  47. package/src/__tests__/buildNormalizedParams.test.ts +1 -1
  48. package/src/__tests__/errors.test.ts +141 -0
  49. package/src/errors.ts +74 -1
  50. package/src/index.ts +10 -10
  51. package/src/makeCancelablePromise.ts +41 -0
  52. package/src/pollJobResult.ts +3 -3
  53. package/src/rawPageIterator.ts +1 -1
  54. package/src/request.ts +5 -5
@@ -0,0 +1,141 @@
1
+ import { ApiError, TimeoutError } from '../errors';
2
+ import { CanceledPromiseError } from '../makeCancelablePromise';
3
+
4
+ function buildApiError() {
5
+ return new ApiError({
6
+ request: {
7
+ url: '/items/bad-id',
8
+ method: 'GET',
9
+ headers: {},
10
+ body: undefined,
11
+ },
12
+ response: {
13
+ status: 422,
14
+ statusText: 'Unprocessable Entity',
15
+ headers: {},
16
+ body: {
17
+ data: [
18
+ {
19
+ id: '1',
20
+ type: 'api_error' as const,
21
+ attributes: {
22
+ code: 'INVALID_FIELD',
23
+ doc_url: 'https://www.datocms.com/docs',
24
+ details: { field: 'item_id' },
25
+ },
26
+ },
27
+ ],
28
+ },
29
+ },
30
+ });
31
+ }
32
+
33
+ function buildTimeoutError() {
34
+ return new TimeoutError({
35
+ request: { url: '/items', method: 'GET', headers: {}, body: undefined },
36
+ });
37
+ }
38
+
39
+ describe('ApiError', () => {
40
+ it('keeps a correct prototype chain and name', () => {
41
+ const error = buildApiError();
42
+
43
+ expect(error).toBeInstanceOf(ApiError);
44
+ expect(error).toBeInstanceOf(Error);
45
+ expect(error.name).toBe('ApiError');
46
+ expect(error.constructor.name).toBe('ApiError');
47
+ });
48
+
49
+ it('exposes request, response and parsed errors', () => {
50
+ const error = buildApiError();
51
+
52
+ expect(error.response.status).toBe(422);
53
+ expect(error.request.url).toBe('/items/bad-id');
54
+ expect(error.errors).toHaveLength(1);
55
+ expect(error.findError('INVALID_FIELD')).toBeTruthy();
56
+ });
57
+
58
+ // These packages ship parallel CJS and ESM builds, so a bundler can load two
59
+ // distinct copies of this module. `instanceof` must still work for an error
60
+ // produced by the other copy.
61
+ it('recognizes an instance coming from a duplicate copy of the module', () => {
62
+ const fromOtherCopy = Object.assign(new Error('API Error!'), {
63
+ name: 'ApiError',
64
+ [Symbol.for('@datocms/rest-client-utils:ApiError')]: true,
65
+ });
66
+
67
+ expect(fromOtherCopy).toBeInstanceOf(ApiError);
68
+ });
69
+
70
+ it('does not recognize unrelated values', () => {
71
+ expect(new Error('nope')).not.toBeInstanceOf(ApiError);
72
+ expect({}).not.toBeInstanceOf(ApiError);
73
+ expect(null).not.toBeInstanceOf(ApiError);
74
+ expect(buildTimeoutError()).not.toBeInstanceOf(ApiError);
75
+ // merely borrowing the name is not enough
76
+ expect(
77
+ Object.assign(new Error('impostor'), { name: 'ApiError' }),
78
+ ).not.toBeInstanceOf(ApiError);
79
+ });
80
+
81
+ it('keeps subclass checks exact', () => {
82
+ class SubclassedApiError extends ApiError {}
83
+
84
+ const parent = buildApiError();
85
+ const child = new SubclassedApiError({
86
+ request: { url: '/items', method: 'GET', headers: {}, body: undefined },
87
+ response: { status: 500, statusText: 'Error', headers: {}, body: {} },
88
+ });
89
+
90
+ expect(child).toBeInstanceOf(SubclassedApiError);
91
+ expect(child).toBeInstanceOf(ApiError);
92
+ expect(parent).not.toBeInstanceOf(SubclassedApiError);
93
+ });
94
+ });
95
+
96
+ describe('TimeoutError', () => {
97
+ it('keeps a correct prototype chain and name', () => {
98
+ const error = buildTimeoutError();
99
+
100
+ expect(error).toBeInstanceOf(TimeoutError);
101
+ expect(error).toBeInstanceOf(Error);
102
+ expect(error.name).toBe('TimeoutError');
103
+ });
104
+
105
+ it('recognizes an instance coming from a duplicate copy of the module', () => {
106
+ const fromOtherCopy = Object.assign(new Error('API Error!'), {
107
+ name: 'TimeoutError',
108
+ [Symbol.for('@datocms/rest-client-utils:TimeoutError')]: true,
109
+ });
110
+
111
+ expect(fromOtherCopy).toBeInstanceOf(TimeoutError);
112
+ });
113
+
114
+ it('is not confused with an ApiError', () => {
115
+ expect(buildApiError()).not.toBeInstanceOf(TimeoutError);
116
+ });
117
+ });
118
+
119
+ describe('CanceledPromiseError', () => {
120
+ it('keeps a correct prototype chain and name', () => {
121
+ const error = new CanceledPromiseError();
122
+
123
+ expect(error).toBeInstanceOf(CanceledPromiseError);
124
+ expect(error).toBeInstanceOf(Error);
125
+ expect(error.name).toBe('CanceledPromiseError');
126
+ });
127
+
128
+ it('recognizes an instance coming from a duplicate copy of the module', () => {
129
+ const fromOtherCopy = Object.assign(new Error('Promise canceled!'), {
130
+ name: 'CanceledPromiseError',
131
+ [Symbol.for('@datocms/rest-client-utils:CanceledPromiseError')]: true,
132
+ });
133
+
134
+ expect(fromOtherCopy).toBeInstanceOf(CanceledPromiseError);
135
+ });
136
+
137
+ it('is not confused with other error types', () => {
138
+ expect(buildApiError()).not.toBeInstanceOf(CanceledPromiseError);
139
+ expect(new Error('nope')).not.toBeInstanceOf(CanceledPromiseError);
140
+ });
141
+ });
package/src/errors.ts CHANGED
@@ -59,6 +59,19 @@ export type ApiErrorResponse = {
59
59
  body?: unknown;
60
60
  };
61
61
 
62
+ /**
63
+ * Brands stamped on error instances.
64
+ *
65
+ * `Symbol.for()` looks up the cross-realm global symbol registry, so every copy
66
+ * of this module — CJS or ESM, inlined by a bundler or not — resolves these to
67
+ * the very same symbols. Since these packages ship parallel CJS and ESM builds
68
+ * with no guarantee that a consumer ends up with a single copy, identity is
69
+ * carried by these brands rather than by class identity alone, so `instanceof`
70
+ * keeps working across duplicated copies.
71
+ */
72
+ const TIMEOUT_ERROR = Symbol.for('@datocms/rest-client-utils:TimeoutError');
73
+ const API_ERROR = Symbol.for('@datocms/rest-client-utils:ApiError');
74
+
62
75
  export type TimeoutErrorInitObject = {
63
76
  request: ApiErrorRequest;
64
77
  preCallStack?: string;
@@ -68,12 +81,44 @@ export class TimeoutError extends Error {
68
81
  request: ApiErrorRequest;
69
82
  preCallStack?: string;
70
83
 
84
+ declare readonly [TIMEOUT_ERROR]: true;
85
+
86
+ static [Symbol.hasInstance](value: unknown): value is TimeoutError {
87
+ if (
88
+ typeof value !== 'object' ||
89
+ value === null ||
90
+ !(TIMEOUT_ERROR in value)
91
+ ) {
92
+ return false;
93
+ }
94
+
95
+ // `this` is the constructor `instanceof` was invoked on: when it is a
96
+ // subclass, defer to a real prototype-chain check so subclasses stay exact.
97
+ // biome-ignore lint/complexity/noThisInStatic: dynamic `this` is required here; hardcoding the class would break subclass checks
98
+ const invokedOn = this as unknown as { prototype: object };
99
+
100
+ if (invokedOn !== TimeoutError) {
101
+ return Object.prototype.isPrototypeOf.call(invokedOn.prototype, value);
102
+ }
103
+
104
+ return true;
105
+ }
106
+
71
107
  constructor(initObject: TimeoutErrorInitObject) {
72
108
  super('API Error!');
73
109
  Object.setPrototypeOf(this, new.target.prototype);
74
110
 
111
+ Object.defineProperty(this, TIMEOUT_ERROR, {
112
+ value: true,
113
+ enumerable: false,
114
+ writable: false,
115
+ configurable: false,
116
+ });
117
+
118
+ this.name = 'TimeoutError';
119
+
75
120
  if ('captureStackTrace' in Error) {
76
- Error.captureStackTrace(this, ApiError);
121
+ Error.captureStackTrace(this, TimeoutError);
77
122
  } else {
78
123
  this.stack = new (Error as any)().stack;
79
124
  }
@@ -100,10 +145,38 @@ export class ApiError extends Error {
100
145
  response: ApiErrorResponse;
101
146
  preCallStack?: string;
102
147
 
148
+ declare readonly [API_ERROR]: true;
149
+
150
+ static [Symbol.hasInstance](value: unknown): value is ApiError {
151
+ if (typeof value !== 'object' || value === null || !(API_ERROR in value)) {
152
+ return false;
153
+ }
154
+
155
+ // `this` is the constructor `instanceof` was invoked on: when it is a
156
+ // subclass, defer to a real prototype-chain check so subclasses stay exact.
157
+ // biome-ignore lint/complexity/noThisInStatic: dynamic `this` is required here; hardcoding the class would break subclass checks
158
+ const invokedOn = this as unknown as { prototype: object };
159
+
160
+ if (invokedOn !== ApiError) {
161
+ return Object.prototype.isPrototypeOf.call(invokedOn.prototype, value);
162
+ }
163
+
164
+ return true;
165
+ }
166
+
103
167
  constructor(initObject: ApiErrorInitObject) {
104
168
  super('API Error!');
105
169
  Object.setPrototypeOf(this, new.target.prototype);
106
170
 
171
+ Object.defineProperty(this, API_ERROR, {
172
+ value: true,
173
+ enumerable: false,
174
+ writable: false,
175
+ configurable: false,
176
+ });
177
+
178
+ this.name = 'ApiError';
179
+
107
180
  if ('captureStackTrace' in Error) {
108
181
  Error.captureStackTrace(this, ApiError);
109
182
  } else {
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
- export * from './deserialize';
2
- export * from './errors';
3
- export * from './makeCancelablePromise';
4
- export * from './pollJobResult';
5
- export * from './rawPageIterator';
6
- export * from './request';
7
- export * from './serialize';
8
- export * from './toId';
9
- export * from './types';
10
- export * from './wait';
1
+ export * from './deserialize.js';
2
+ export * from './errors.js';
3
+ export * from './makeCancelablePromise.js';
4
+ export * from './pollJobResult.js';
5
+ export * from './rawPageIterator.js';
6
+ export * from './request.js';
7
+ export * from './serialize.js';
8
+ export * from './toId.js';
9
+ export * from './types.js';
10
+ export * from './wait.js';
@@ -1,7 +1,48 @@
1
+ /**
2
+ * See the note on the brands in `errors.ts`: identity is carried by a symbol
3
+ * from the cross-realm global registry so that `instanceof` keeps working even
4
+ * when a bundler ends up loading more than one copy of this module.
5
+ */
6
+ const CANCELED_PROMISE_ERROR = Symbol.for(
7
+ '@datocms/rest-client-utils:CanceledPromiseError',
8
+ );
9
+
1
10
  export class CanceledPromiseError extends Error {
11
+ declare readonly [CANCELED_PROMISE_ERROR]: true;
12
+
13
+ static [Symbol.hasInstance](value: unknown): value is CanceledPromiseError {
14
+ if (
15
+ typeof value !== 'object' ||
16
+ value === null ||
17
+ !(CANCELED_PROMISE_ERROR in value)
18
+ ) {
19
+ return false;
20
+ }
21
+
22
+ // `this` is the constructor `instanceof` was invoked on: when it is a
23
+ // subclass, defer to a real prototype-chain check so subclasses stay exact.
24
+ // biome-ignore lint/complexity/noThisInStatic: dynamic `this` is required here; hardcoding the class would break subclass checks
25
+ const invokedOn = this as unknown as { prototype: object };
26
+
27
+ if (invokedOn !== CanceledPromiseError) {
28
+ return Object.prototype.isPrototypeOf.call(invokedOn.prototype, value);
29
+ }
30
+
31
+ return true;
32
+ }
33
+
2
34
  constructor() {
3
35
  super('Promise canceled!');
4
36
  Object.setPrototypeOf(this, new.target.prototype);
37
+
38
+ Object.defineProperty(this, CANCELED_PROMISE_ERROR, {
39
+ value: true,
40
+ enumerable: false,
41
+ writable: false,
42
+ configurable: false,
43
+ });
44
+
45
+ this.name = 'CanceledPromiseError';
5
46
  }
6
47
  }
7
48
 
@@ -1,6 +1,6 @@
1
- import { ApiError } from './errors';
2
- import type { JobResult } from './internalTypes';
3
- import { wait } from './wait';
1
+ import { ApiError } from './errors.js';
2
+ import type { JobResult } from './internalTypes.js';
3
+ import { wait } from './wait.js';
4
4
 
5
5
  export async function pollJobResult(
6
6
  fetcher: () => Promise<JobResult>,
@@ -1,5 +1,5 @@
1
1
  import { Scheduler } from 'async-scheduler';
2
- import { deserializeRawItem } from './deserialize';
2
+ import { deserializeRawItem } from './deserialize.js';
3
3
 
4
4
  export type IteratorOptions = {
5
5
  perPage?: number;
package/src/request.ts CHANGED
@@ -1,18 +1,18 @@
1
1
  /// <reference lib="dom" />
2
2
 
3
- import { buildNormalizedParams } from './buildNormalizedParams';
3
+ import { buildNormalizedParams } from './buildNormalizedParams.js';
4
4
  import {
5
5
  ApiError,
6
6
  type ApiErrorInitObject,
7
7
  TimeoutError,
8
8
  type TimeoutErrorInitObject,
9
- } from './errors';
10
- import type { JobResult } from './internalTypes';
9
+ } from './errors.js';
10
+ import type { JobResult } from './internalTypes.js';
11
11
  import {
12
12
  CanceledPromiseError,
13
13
  makeCancelablePromise,
14
- } from './makeCancelablePromise';
15
- import { wait } from './wait';
14
+ } from './makeCancelablePromise.js';
15
+ import { wait } from './wait.js';
16
16
 
17
17
  const isBrowser =
18
18
  typeof window !== 'undefined' && typeof window.document !== 'undefined';