Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
CHANGELOG
=========

7.0.0
7.0.0 (unreleased)
------------------

* **Breaking** Dropped support for Node.js 18 and 20. Node.js 22 or greater is now
Expand All @@ -14,6 +14,21 @@ CHANGELOG
property (for example, the network error behind a `FETCH_ERROR`). The
`WebServiceError` class and the `WebServiceClientError` type are now exported
from the package.
* The `code` property on `WebServiceError` and the `WebServiceClientError`
interface is now typed as `WebServiceErrorCode`
(`ClientErrorCode | (string & {})`) instead of `string`, providing
autocompletion for the client-generated codes while still accepting any
code returned by the web service. The `ClientErrorCode` and
`WebServiceErrorCode` types are exported from the package.
* The `AddressNotFoundError`, `BadMethodCallError`, `InvalidDbBufferError`,
and `ValueError` classes now accept an optional `cause` and forward it to
`Error`. `Reader.openBuffer()` now preserves the underlying parsing error as
the `cause` of the `InvalidDbBufferError` it throws.
* Added a `fetcher` option to the `WebServiceClient` options, allowing a
custom `fetch` implementation to be supplied (for example, to route requests
through a custom dispatcher or proxy, or for testing). It defaults to the
global `fetch`. The `WebServiceClientOptions` type is exported from the
package.

6.3.4 (2025-11-25)
------------------
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ your MaxMind `accountID` and `licenseKey` as parameters. The third argument is
an object holding additional option. The `timeout` option defaults to `3000`.
The `host` option defaults to `geoip.maxmind.com`. Set `host` to `geolite.info`
to use the GeoLite web service instead of GeoIP. Set `host` to
`sandbox.maxmind.com` to use the Sandbox environment.
`sandbox.maxmind.com` to use the Sandbox environment. The `fetcher` option lets
you supply a custom `fetch` implementation (for example, to route requests
through a proxy or custom dispatcher); it defaults to the global `fetch`.

You may then call the function corresponding to a specific end point, passing it
the IP address you want to lookup.
Expand Down
97 changes: 0 additions & 97 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"eslint-config-prettier": "^10.0.1",
"globals": "^17.0.0",
"jest": "^30.0.0",
"nock": "^14.0.0-beta.15",
"prettier": "^3.0.0",
"ts-jest": "^29.4.0",
"typedoc": "^0.28.1",
Expand Down
37 changes: 36 additions & 1 deletion src/errors.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,39 @@
import { WebServiceError } from './errors.js';
import {
AddressNotFoundError,
BadMethodCallError,
InvalidDbBufferError,
ValueError,
WebServiceError,
} from './errors.js';

describe.each([
['AddressNotFoundError', AddressNotFoundError],
['BadMethodCallError', BadMethodCallError],
['InvalidDbBufferError', InvalidDbBufferError],
['ValueError', ValueError],
] as const)('%s', (name, ErrorClass) => {
it('uses the message and sets the name', () => {
const err = new ErrorClass('boom');

expect(err).toBeInstanceOf(Error);
expect(err).toBeInstanceOf(ErrorClass);
expect(err.message).toBe('boom');
expect(err.name).toBe(name);
});

it('preserves the underlying cause when provided', () => {
const cause = new TypeError('underlying');
const err = new ErrorClass('boom', { cause });

expect(err.cause).toBe(cause);
});

it('leaves cause undefined when not provided', () => {
const err = new ErrorClass('boom');

expect(err.cause).toBeUndefined();
});
});

describe('WebServiceError', () => {
it('is an Error instance', () => {
Expand Down
25 changes: 13 additions & 12 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WebServiceClientError } from './types.js';
import { WebServiceClientError, WebServiceErrorCode } from './types.js';

/* tslint:disable:max-classes-per-file */

Expand All @@ -15,7 +15,7 @@ export class WebServiceError extends Error implements WebServiceClientError {
/**
* The error code returned by the web service or generated by this client.
*/
public readonly code: string;
public readonly code: WebServiceErrorCode;
/**
* A human-readable description of the error. This is an alias of `message`,
* retained for backward compatibility.
Expand All @@ -36,7 +36,7 @@ export class WebServiceError extends Error implements WebServiceClientError {

constructor(
properties: {
code: string;
code: WebServiceErrorCode;
error: string;
status?: number;
url: string;
Expand Down Expand Up @@ -66,8 +66,8 @@ WebServiceError.prototype.name = 'WebServiceError';
* This generally means that the address was a private or reserved address.
*/
export class AddressNotFoundError extends Error {
constructor(message: string) {
super(message);
constructor(message: string, options?: { cause?: unknown }) {
super(message, options);
this.name = this.constructor.name;
}
}
Expand All @@ -77,8 +77,8 @@ export class AddressNotFoundError extends Error {
* e.g. `reader.city` is used with a Country database
*/
export class BadMethodCallError extends Error {
constructor(message: string) {
super(message);
constructor(message: string, options?: { cause?: unknown }) {
super(message, options);
this.name = this.constructor.name;
}
}
Expand All @@ -87,18 +87,19 @@ export class BadMethodCallError extends Error {
* This error is thrown if a database buffer is not a valid database
*/
export class InvalidDbBufferError extends Error {
constructor(message: string) {
super(message);
constructor(message: string, options?: { cause?: unknown }) {
super(message, options);
this.name = this.constructor.name;
}
}

/**
* This error is thrown if the IP address provided is not valid.
* This error is thrown when an argument is invalid, such as an invalid IP
* address or an invalid `WebServiceClient` option.
*/
export class ValueError extends Error {
constructor(message: string) {
super(message);
constructor(message: string, options?: { cause?: unknown }) {
super(message, options);
this.name = this.constructor.name;
}
}
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ export { Reader, ReaderModel, WebServiceClient };
export * from './records.js';
export * from './models/index.js';
export * from './errors.js';
export type { WebServiceClientError } from './types.js';
export type {
ClientErrorCode,
WebServiceClientError,
WebServiceErrorCode,
} from './types.js';
export type { WebServiceClientOptions } from './webServiceClient.js';
13 changes: 13 additions & 0 deletions src/reader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,18 @@ describe('Reader', () => {
InvalidDbBufferError
);
});

it('preserves the underlying error as the cause', () => {
expect.assertions(3);

try {
Reader.openBuffer(Buffer.from('foo'));
} catch (e) {
expect(e).toBeInstanceOf(InvalidDbBufferError);
const err = e as InvalidDbBufferError;
expect(typeof err.message).toBe('string');
expect(err.cause).toBeInstanceOf(Error);
}
});
});
});
6 changes: 3 additions & 3 deletions src/reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export default class Reader {
let reader;
try {
reader = new mmdb.Reader(buffer);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
throw new InvalidDbBufferError(e);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
throw new InvalidDbBufferError(error.message, { cause: error });
}

return new ReaderModel(reader);
Expand Down
4 changes: 3 additions & 1 deletion src/readerModel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
BadMethodCallError,
ValueError,
} from './errors.js';
import * as models from './models/index.js';
import Reader from './reader.js';

describe('ReaderModel', () => {
Expand Down Expand Up @@ -600,7 +601,8 @@ describe('ReaderModel', () => {
'./test/data/test-data/GeoIP2-Enterprise-Test.mmdb'
);

const model = reader.enterprise('2.125.160.216');
// enterprise() should be typed as Enterprise, not the wider City.
const model: models.Enterprise = reader.enterprise('2.125.160.216');

const expected = {
city: {
Expand Down
2 changes: 1 addition & 1 deletion src/readerModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export default class ReaderModel {
* @throws {AddressNotFoundError} Throws an error when the IP address isn't found in the database
* @throws {ValueError} Throws an error when the IP address isn't valid
*/
public enterprise(ipAddress: string): models.City {
public enterprise(ipAddress: string): models.Enterprise {
return this.modelFor(
models.Enterprise,
'Enterprise',
Expand Down
Loading