Skip to content
Open
16 changes: 15 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,20 @@ 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`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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
22 changes: 11 additions & 11 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,8 +87,8 @@ 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;
}
}
Expand All @@ -97,8 +97,8 @@ export class InvalidDbBufferError extends Error {
* This error is thrown if the IP address provided is not valid.
*/
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
5 changes: 2 additions & 3 deletions src/records.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { ConnectionType } from './types.js';

/**
* The name of the place based on the locales list passed to the
* `WebServiceClient` constructor. Don't use any of these names as a database or
* dictionary key. Use the ID or relevant code instead.
* The name of the place in each available locale. Don't use any of these names
* as a database or dictionary key. Use the ID or relevant code instead.
*/
export interface Names {
readonly de?: string;
Expand Down
Loading