@merchi/sdk 1.42.20 → 1.42.22

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.
@@ -23,4 +23,4 @@ export declare enum ErrorType {
23
23
  BAD_SMS_TOKEN = 21,
24
24
  DOMAIN_NOT_FOUND = 22
25
25
  }
26
- export declare function getErrorFromCode(code: number): ErrorType;
26
+ export declare function getErrorFromCode(code: unknown): number;
@@ -25,7 +25,9 @@ export var ErrorType;
25
25
  ErrorType[ErrorType["DOMAIN_NOT_FOUND"] = 22] = "DOMAIN_NOT_FOUND";
26
26
  })(ErrorType || (ErrorType = {}));
27
27
  export function getErrorFromCode(code) {
28
- if (code in ErrorType) {
28
+ // The API can add error codes before this SDK's named constants are updated.
29
+ // Preserve those numeric diagnostics instead of turning them into UNKNOWN.
30
+ if (typeof code === 'number' && Number.isSafeInteger(code) && code >= 0) {
29
31
  return code;
30
32
  }
31
33
  return ErrorType.UNKNOWN_ERROR;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import { ErrorType, getErrorFromCode } from './errors.js';
2
+ test.each([ErrorType.UNKNOWN_ERROR, ErrorType.INVALID_PHONE_NUMBER, ErrorType.DOMAIN_NOT_FOUND])('preserves known API error code %s', function (code) {
3
+ expect(getErrorFromCode(code)).toBe(code);
4
+ });
5
+ // INVALID_HCAPTCHA and SPAM_DETECTED exist in the API's Python SDK but not
6
+ // this SDK's legacy enum. Future codes must remain diagnosable too.
7
+ test.each([226, 227, 1000, Number.MAX_SAFE_INTEGER])('preserves forward-compatible numeric API error code %s', function (code) {
8
+ expect(getErrorFromCode(code)).toBe(code);
9
+ });
10
+ test.each([
11
+ undefined, null, -1, 1.5, NaN, Infinity, -Infinity,
12
+ Number.MAX_SAFE_INTEGER + 1, '227', 'INVALID_PHONE_NUMBER',
13
+ 'toString', '__proto__', true, {}, [],
14
+ ])('uses UNKNOWN_ERROR for malformed API code %p', function (code) {
15
+ expect(getErrorFromCode(code)).toBe(ErrorType.UNKNOWN_ERROR);
16
+ });
@@ -1,3 +1,18 @@
1
+ var __extends = (this && this.__extends) || (function () {
2
+ var extendStatics = function (d, b) {
3
+ extendStatics = Object.setPrototypeOf ||
4
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
5
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
6
+ return extendStatics(d, b);
7
+ };
8
+ return function (d, b) {
9
+ if (typeof b !== "function" && b !== null)
10
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
11
+ extendStatics(d, b);
12
+ function __() { this.constructor = d; }
13
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
14
+ };
15
+ })();
1
16
  import { Merchi } from '../merchi.js';
2
17
  import { setup, mockFetch } from '../test_util.js';
3
18
  setup();
@@ -55,6 +70,60 @@ test('preserves isTest', function () {
55
70
  entity.fromJson({ id: 1, isTest: true });
56
71
  expect(entity.toJson()).toMatchObject({ isTest: true });
57
72
  });
73
+ test('serialises a new cart when FormData has no set', function () {
74
+ var AppendOnlyFormData = /** @class */ (function () {
75
+ function AppendOnlyFormData() {
76
+ this.fields = new Map();
77
+ }
78
+ AppendOnlyFormData.prototype.append = function (name, value) {
79
+ this.fields.set(name, String(value));
80
+ };
81
+ AppendOnlyFormData.prototype.get = function (name) {
82
+ return this.fields.has(name) ? this.fields.get(name) : null;
83
+ };
84
+ AppendOnlyFormData.prototype.entries = function () {
85
+ return this.fields.entries();
86
+ };
87
+ return AppendOnlyFormData;
88
+ }());
89
+ var ReplacingFormData = /** @class */ (function (_super) {
90
+ __extends(ReplacingFormData, _super);
91
+ function ReplacingFormData() {
92
+ var _this = _super !== null && _super.apply(this, arguments) || this;
93
+ _this.ready = false;
94
+ return _this;
95
+ }
96
+ ReplacingFormData.prototype.append = function (name, value) {
97
+ if (!this.ready) {
98
+ throw new Error("delete was not called before append(".concat(name, ")"));
99
+ }
100
+ this.ready = false;
101
+ _super.prototype.append.call(this, name, value);
102
+ };
103
+ ReplacingFormData.prototype.delete = function () {
104
+ this.ready = true;
105
+ };
106
+ return ReplacingFormData;
107
+ }(AppendOnlyFormData));
108
+ var serialise = function (form) {
109
+ var merchi = new Merchi();
110
+ var cart = new merchi.Cart();
111
+ cart.isTest = false;
112
+ var domain = new merchi.Domain();
113
+ domain.id = 9;
114
+ cart.domain = domain;
115
+ cart.toFormData({ existing: form });
116
+ return form;
117
+ };
118
+ var appended = serialise(new AppendOnlyFormData());
119
+ expect(appended.get('isTest')).toBe('false');
120
+ expect(appended.get('domain-0-id')).toBe('9');
121
+ expect(appended.get('domain-count')).toBe('1');
122
+ var replaced = serialise(new ReplacingFormData());
123
+ expect(replaced.get('isTest')).toBe('false');
124
+ expect(replaced.get('domain-0-id')).toBe('9');
125
+ expect(replaced.get('domain-count')).toBe('1');
126
+ });
58
127
  test('preserves source attribution', function () {
59
128
  var merchi = new Merchi();
60
129
  var entity = new merchi.Cart();
package/dist/entity.js CHANGED
@@ -302,6 +302,19 @@ var Entity = /** @class */ (function () {
302
302
  if (fileIndex === undefined) {
303
303
  fileIndex = { value: 0 };
304
304
  }
305
+ // Prefer set so a repeated key is replaced. Fall back to append for
306
+ // FormData implementations that never added set.
307
+ var writeField = function (target, name, value) {
308
+ if (typeof target.set === 'function') {
309
+ target.set(name, value);
310
+ return;
311
+ }
312
+ var legacy = target;
313
+ if (typeof legacy.delete === 'function') {
314
+ legacy.delete(name);
315
+ }
316
+ target.append(name, value);
317
+ };
305
318
  var appendData = function (name, value) {
306
319
  /* istanbul ignore next */
307
320
  if (name === undefined || value === undefined) {
@@ -311,10 +324,10 @@ var Entity = /** @class */ (function () {
311
324
  if (prefix) {
312
325
  name = prefix + '-' + name;
313
326
  }
314
- result.set(name, value);
327
+ writeField(result, name, value);
315
328
  };
316
329
  if (_this.fileData !== undefined) {
317
- result.set(String(fileIndex.value), _this.fileData);
330
+ writeField(result, String(fileIndex.value), _this.fileData);
318
331
  appendData('fileDataIndex', fileIndex.value);
319
332
  fileIndex.value++;
320
333
  }
package/dist/request.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { ErrorType } from './constants/errors.js';
2
1
  export interface RequestOptions extends RequestInit {
3
2
  query?: string[][];
4
3
  }
@@ -37,7 +36,8 @@ export interface ApiErrorDetails {
37
36
  }
38
37
  export declare class ApiError extends Error {
39
38
  statusCode?: number;
40
- errorCode?: ErrorType;
39
+ /** Numeric API code, including values added after this SDK was published. */
40
+ errorCode?: number;
41
41
  errorMessage: string;
42
42
  /**
43
43
  * Structured context the API may include alongside the error
package/dist/request.js CHANGED
@@ -71,7 +71,6 @@ var __values = (this && this.__values) || function(o) {
71
71
  };
72
72
  throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
73
73
  };
74
- // eslint-disable-next-line no-unused-vars
75
74
  import { getErrorFromCode } from './constants/errors.js';
76
75
  function resolveApiErrorMessage(err) {
77
76
  if (err && typeof err === 'object' && typeof err.message === 'string' && err.message) {
@@ -83,6 +83,41 @@ test('will get default errorCode', function () {
83
83
  expect(e.message).toBe('just a test');
84
84
  });
85
85
  });
86
+ test('lead spam rejection keeps its API code through apiFetch', function () { return __awaiter(void 0, void 0, void 0, function () {
87
+ return __generator(this, function (_a) {
88
+ switch (_a.label) {
89
+ case 0:
90
+ mockFetch(false, {
91
+ statusCode: 400,
92
+ errorCode: 227,
93
+ message: 'Invalid submission',
94
+ }, 400);
95
+ return [4 /*yield*/, expect(apiFetch('https://api.merchi.co/', '/create_new_lead/', {
96
+ method: 'POST',
97
+ })).rejects.toMatchObject({
98
+ name: 'ApiError',
99
+ statusCode: 400,
100
+ errorCode: 227,
101
+ message: 'Invalid submission',
102
+ errorMessage: 'Invalid submission',
103
+ })];
104
+ case 1:
105
+ _a.sent();
106
+ return [2 /*return*/];
107
+ }
108
+ });
109
+ }); });
110
+ test.each([226, 1000])('ApiError preserves new numeric API code %s', function (errorCode) {
111
+ var error = new ApiError({ statusCode: 400, errorCode: errorCode, message: 'Rejected' });
112
+ expect(error.errorCode).toBe(errorCode);
113
+ expect(error.original.errorCode).toBe(errorCode);
114
+ });
115
+ test.each(['227', 'INVALID_PHONE_NUMBER', NaN, -1])('ApiError rejects malformed API code %p', function (errorCode) {
116
+ var error = new ApiError({ statusCode: 400, errorCode: errorCode, message: 'Rejected' });
117
+ expect(error.errorCode).toBe(ErrorType.UNKNOWN_ERROR);
118
+ expect(error.statusCode).toBe(400);
119
+ expect(error.message).toBe('Rejected');
120
+ });
86
121
  test('ApiError keeps plain string messages', function () {
87
122
  var e = new ApiError('empty response');
88
123
  expect(e.message).toBe('empty response');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merchi/sdk",
3
- "version": "1.42.20",
3
+ "version": "1.42.22",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,23 @@
1
+ import { ErrorType, getErrorFromCode } from './errors.js';
2
+
3
+ test.each([ErrorType.UNKNOWN_ERROR, ErrorType.INVALID_PHONE_NUMBER, ErrorType.DOMAIN_NOT_FOUND])(
4
+ 'preserves known API error code %s', code => {
5
+ expect(getErrorFromCode(code)).toBe(code);
6
+ },
7
+ );
8
+
9
+ // INVALID_HCAPTCHA and SPAM_DETECTED exist in the API's Python SDK but not
10
+ // this SDK's legacy enum. Future codes must remain diagnosable too.
11
+ test.each([226, 227, 1000, Number.MAX_SAFE_INTEGER])(
12
+ 'preserves forward-compatible numeric API error code %s', code => {
13
+ expect(getErrorFromCode(code)).toBe(code);
14
+ },
15
+ );
16
+
17
+ test.each([
18
+ undefined, null, -1, 1.5, NaN, Infinity, -Infinity,
19
+ Number.MAX_SAFE_INTEGER + 1, '227', 'INVALID_PHONE_NUMBER',
20
+ 'toString', '__proto__', true, {}, [],
21
+ ])('uses UNKNOWN_ERROR for malformed API code %p', code => {
22
+ expect(getErrorFromCode(code)).toBe(ErrorType.UNKNOWN_ERROR);
23
+ });
@@ -24,8 +24,10 @@ export enum ErrorType {
24
24
  DOMAIN_NOT_FOUND = 22,
25
25
  }
26
26
 
27
- export function getErrorFromCode(code: number): ErrorType {
28
- if (code in ErrorType) {
27
+ export function getErrorFromCode(code: unknown): number {
28
+ // The API can add error codes before this SDK's named constants are updated.
29
+ // Preserve those numeric diagnostics instead of turning them into UNKNOWN.
30
+ if (typeof code === 'number' && Number.isSafeInteger(code) && code >= 0) {
29
31
  return code;
30
32
  }
31
33
  return ErrorType.UNKNOWN_ERROR;
@@ -63,6 +63,54 @@ test('preserves isTest', () => {
63
63
  expect(entity.toJson()).toMatchObject({ isTest: true });
64
64
  });
65
65
 
66
+ test('serialises a new cart when FormData has no set', () => {
67
+ class AppendOnlyFormData {
68
+ fields = new Map<string, string>();
69
+ append(name: string, value: any) {
70
+ this.fields.set(name, String(value));
71
+ }
72
+ get(name: string) {
73
+ return this.fields.has(name) ? this.fields.get(name) : null;
74
+ }
75
+ entries() {
76
+ return this.fields.entries();
77
+ }
78
+ }
79
+ class ReplacingFormData extends AppendOnlyFormData {
80
+ private ready = false;
81
+ append(name: string, value: any) {
82
+ if (!this.ready) {
83
+ throw new Error(`delete was not called before append(${name})`);
84
+ }
85
+ this.ready = false;
86
+ super.append(name, value);
87
+ }
88
+ delete() {
89
+ this.ready = true;
90
+ }
91
+ }
92
+ const serialise = (form: AppendOnlyFormData) => {
93
+ const merchi = new Merchi();
94
+ const cart = new merchi.Cart();
95
+ cart.isTest = false;
96
+ const domain = new merchi.Domain();
97
+ domain.id = 9;
98
+ cart.domain = domain;
99
+ cart.toFormData({existing: form as unknown as FormData});
100
+ return form;
101
+ };
102
+
103
+ const appended = serialise(new AppendOnlyFormData());
104
+ expect(appended.get('isTest')).toBe('false');
105
+ expect(appended.get('domain-0-id')).toBe('9');
106
+ expect(appended.get('domain-count')).toBe('1');
107
+
108
+ const replaced = serialise(new ReplacingFormData());
109
+ expect(replaced.get('isTest')).toBe('false');
110
+ expect(replaced.get('domain-0-id')).toBe('9');
111
+ expect(replaced.get('domain-count')).toBe('1');
112
+ });
113
+
66
114
  test('preserves source attribution', () => {
67
115
  const merchi = new Merchi();
68
116
  const entity = new merchi.Cart();
package/src/entity.ts CHANGED
@@ -928,6 +928,19 @@ export class Entity {
928
928
  if (fileIndex === undefined) {
929
929
  fileIndex = {value: 0};
930
930
  }
931
+ // Prefer set so a repeated key is replaced. Fall back to append for
932
+ // FormData implementations that never added set.
933
+ const writeField = (target: FormData, name: string, value: any) => {
934
+ if (typeof target.set === 'function') {
935
+ target.set(name, value);
936
+ return;
937
+ }
938
+ const legacy = target as FormData & {delete?: (field: string) => void};
939
+ if (typeof legacy.delete === 'function') {
940
+ legacy.delete(name);
941
+ }
942
+ target.append(name, value);
943
+ };
931
944
  const appendData = (name: string, value: any) => {
932
945
  /* istanbul ignore next */
933
946
  if (name === undefined || value === undefined) {
@@ -937,10 +950,10 @@ export class Entity {
937
950
  if (prefix) {
938
951
  name = prefix + '-' + name;
939
952
  }
940
- result.set(name, value);
953
+ writeField(result, name, value);
941
954
  };
942
955
  if ((this as any).fileData !== undefined) {
943
- result.set(String(fileIndex.value), (this as any).fileData);
956
+ writeField(result, String(fileIndex.value), (this as any).fileData);
944
957
  appendData('fileDataIndex', fileIndex.value);
945
958
  fileIndex.value++;
946
959
  }
@@ -43,6 +43,38 @@ test('will get default errorCode', () => {
43
43
  });
44
44
  });
45
45
 
46
+ test('lead spam rejection keeps its API code through apiFetch', async () => {
47
+ mockFetch(false, {
48
+ statusCode: 400,
49
+ errorCode: 227,
50
+ message: 'Invalid submission',
51
+ }, 400);
52
+ await expect(apiFetch('https://api.merchi.co/', '/create_new_lead/', {
53
+ method: 'POST',
54
+ })).rejects.toMatchObject({
55
+ name: 'ApiError',
56
+ statusCode: 400,
57
+ errorCode: 227,
58
+ message: 'Invalid submission',
59
+ errorMessage: 'Invalid submission',
60
+ });
61
+ });
62
+
63
+ test.each([226, 1000])('ApiError preserves new numeric API code %s', errorCode => {
64
+ const error = new ApiError({ statusCode: 400, errorCode, message: 'Rejected' });
65
+ expect(error.errorCode).toBe(errorCode);
66
+ expect(error.original.errorCode).toBe(errorCode);
67
+ });
68
+
69
+ test.each(['227', 'INVALID_PHONE_NUMBER', NaN, -1])(
70
+ 'ApiError rejects malformed API code %p', errorCode => {
71
+ const error = new ApiError({ statusCode: 400, errorCode, message: 'Rejected' });
72
+ expect(error.errorCode).toBe(ErrorType.UNKNOWN_ERROR);
73
+ expect(error.statusCode).toBe(400);
74
+ expect(error.message).toBe('Rejected');
75
+ },
76
+ );
77
+
46
78
  test('ApiError keeps plain string messages', () => {
47
79
  const e = new ApiError('empty response');
48
80
  expect(e.message).toBe('empty response');
package/src/request.ts CHANGED
@@ -1,5 +1,4 @@
1
- // eslint-disable-next-line no-unused-vars
2
- import { ErrorType, getErrorFromCode } from './constants/errors.js';
1
+ import { getErrorFromCode } from './constants/errors.js';
3
2
 
4
3
  export interface RequestOptions extends RequestInit {
5
4
  query?: string[][];
@@ -50,7 +49,8 @@ function resolveApiErrorMessage(err: any): string {
50
49
 
51
50
  export class ApiError extends Error {
52
51
  public statusCode?: number;
53
- public errorCode?: ErrorType;
52
+ /** Numeric API code, including values added after this SDK was published. */
53
+ public errorCode?: number;
54
54
  public errorMessage: string;
55
55
  /**
56
56
  * Structured context the API may include alongside the error