Skip to content

Commit 06c10ab

Browse files
committed
wip: tests and documentation
1 parent 46c9813 commit 06c10ab

File tree

15 files changed

+506
-88
lines changed

15 files changed

+506
-88
lines changed

packages/ip-location-api/src/functions/lookup.ts

Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,149 @@ import { join } from 'node:path'
55
import { binarySearch, getSmallMemoryFile, type IpLocationApiSettings, type LocalDatabase, number37ToString, parseIp, SAVED_SETTINGS } from '@iplookup/util'
66
import { LOADED_DATA } from './reload.js'
77

8+
/**
9+
* Represents geographical data for an IP address
10+
*/
811
interface GeoData {
12+
/**
13+
* The approximate WGS84 latitude of the IP address
14+
*
15+
* @see https://en.wikipedia.org/wiki/World_Geodetic_System
16+
*/
917
latitude?: number
18+
/**
19+
* The approximate WGS84 longitude of the IP address
20+
*
21+
* @see https://en.wikipedia.org/wiki/World_Geodetic_System
22+
*/
1023
longitude?: number
24+
/**
25+
* The region-specific postcode nearest to the IP address
26+
*/
1127
postcode?: string
28+
/**
29+
* The radius in kilometers of the specified location where the IP address is likely to be
30+
*/
1231
area?: number
32+
/**
33+
* The country of the IP address
34+
* @example 'US'
35+
* @see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
36+
*/
1337
country?: string
38+
/**
39+
* Whether the country is a member of the European Union
40+
*
41+
* @requires country
42+
*/
1443
eu?: boolean
44+
/**
45+
* The first region of the IP address
46+
* @example 'NL-ZH' // (Netherlands, South Holland)
47+
* @see https://en.wikipedia.org/wiki/ISO_3166-2
48+
*/
1549
region1?: string
50+
/**
51+
* The name of the first region of the IP address (Can be aquired in multiple languages using the `language` setting)
52+
* @example 'South Holland'
53+
*/
1654
region1_name?: string
55+
/**
56+
* The second region of the IP address
57+
* @example 'NL-ZH' // (Netherlands, South Holland)
58+
* @see https://en.wikipedia.org/wiki/ISO_3166-2
59+
*/
1760
region2?: string
61+
/**
62+
* The name of the second region of the IP address (Can be aquired in multiple languages using the `language` setting)
63+
* @example 'South Holland'
64+
*/
1865
region2_name?: string
66+
/**
67+
* The metropolitan area of the IP address (Google AdWords API)
68+
* @example 1000
69+
* @see https://developers.google.com/adwords/api/docs/appendix/cities-DMAregions
70+
*/
1971
metro?: number
72+
/**
73+
* The timezone of the IP address
74+
* @example 'Europe/Amsterdam'
75+
*/
2076
timezone?: string
77+
/**
78+
* The city of the IP address
79+
* @example 'Amsterdam'
80+
*/
2181
city?: string
82+
/**
83+
* The name of the country of the IP address (In English)
84+
* @example 'Netherlands'
85+
* @requires country
86+
* @requires country-list (optional peer dependency)
87+
*/
2288
country_name?: string
89+
/**
90+
* The name of the country of the IP address (In the native language of the country)
91+
* @example 'Nederland'
92+
* @requires country
93+
* @requires country-list (optional peer dependency)
94+
*/
2395
country_native?: string
96+
/**
97+
* The continent of the IP address (alpha-2 code)
98+
* @example 'EU'
99+
* @requires country
100+
* @requires country-list (optional peer dependency)
101+
*/
24102
continent?: string
103+
/**
104+
* The name of the continent of the IP address (In English)
105+
* @example 'Europe'
106+
* @requires country
107+
* @requires country-list (optional peer dependency)
108+
*/
25109
continent_name?: string
110+
/**
111+
* The capital of the country of the IP address (In English)
112+
* @example 'Amsterdam'
113+
* @requires country
114+
* @requires country-list (optional peer dependency)
115+
*/
26116
capital?: string
117+
/**
118+
* The phone codes of the country of the IP address
119+
* @example ['31']
120+
* @requires country
121+
* @requires country-list (optional peer dependency)
122+
*/
27123
phone?: number[]
124+
/**
125+
* The currency of the country of the IP address
126+
* @example ['EUR']
127+
* @requires country
128+
* @requires country-list (optional peer dependency)
129+
*/
28130
currency?: string[]
131+
/**
132+
* The languages of the country of the IP address
133+
* @example ['nl']
134+
*/
29135
languages?: string[]
30136
}
31137

138+
/**
139+
* Looks up geographical data for a given IP address
140+
* @param ip - The IP address to look up
141+
* @returns A Promise that resolves to GeoData or null if not found
142+
*/
32143
export async function lookup(ip: string): Promise<GeoData | null> {
33144
//* We don't use net.isIP(ip) as it is slow for ipv6
34145
const { version, ip: ipNumber } = parseIp(ip)
35146

36147
const settings = SAVED_SETTINGS
37148
const db = version === 4 ? settings.v4 : settings.v6
38149

39-
if (!db.loadedData)
40-
return null
41-
if (!(ipNumber >= db.loadedData.firstIp))
150+
if (!db.loadedData || !(ipNumber >= db.loadedData.firstIp))
42151
return null
43152
const list = db.loadedData.startIps
44153
const line = binarySearch(list, ipNumber)
@@ -71,6 +180,13 @@ export async function lookup(ip: string): Promise<GeoData | null> {
71180
return setCityInfo(db.loadedData.mainBuffer!, line * db.recordSize, settings)
72181
}
73182

183+
/**
184+
* Reads a specific line from a file in the database
185+
* @param line - The line number to read
186+
* @param db - The database object
187+
* @param settings - The API settings
188+
* @returns A Promise that resolves to a Buffer containing the line data
189+
*/
74190
async function lineToFile(line: number, db: LocalDatabase, settings: IpLocationApiSettings): Promise<Buffer> {
75191
const [dir, file, offset] = getSmallMemoryFile(line, db)
76192
const fd = await open(join(settings.fieldDir, dir, file), 'r')
@@ -82,13 +198,24 @@ async function lineToFile(line: number, db: LocalDatabase, settings: IpLocationA
82198
return buffer
83199
}
84200

201+
/**
202+
* Extracts city information from a buffer
203+
* @param buffer - The buffer containing city data
204+
* @param offset - The starting offset in the buffer
205+
* @param settings - The API settings
206+
* @returns A Promise that resolves to GeoData
207+
*/
85208
function setCityInfo(buffer: Buffer, offset: number, settings: IpLocationApiSettings): Promise<GeoData> {
86209
let locationId: number | undefined
87210
const geodata: GeoData = {}
211+
212+
//* Read location ID if location file is available
88213
if (settings.locationFile) {
89214
locationId = buffer.readUInt32LE(offset)
90215
offset += 4
91216
}
217+
218+
//* Read latitude and longitude if included in fields
92219
if (settings.fields.includes('latitude')) {
93220
geodata.latitude = buffer.readInt32LE(offset) / 10000
94221
offset += 4
@@ -97,6 +224,8 @@ function setCityInfo(buffer: Buffer, offset: number, settings: IpLocationApiSett
97224
geodata.longitude = buffer.readInt32LE(offset) / 10000
98225
offset += 4
99226
}
227+
228+
//* Read and decode postcode if included in fields
100229
if (settings.fields.includes('postcode')) {
101230
const postcodeLength = buffer.readUInt32LE(offset)
102231
const postcodeValue = buffer.readInt8(offset + 4)
@@ -128,6 +257,8 @@ function setCityInfo(buffer: Buffer, offset: number, settings: IpLocationApiSett
128257
}
129258
offset += 5
130259
}
260+
261+
//* Read area if included in fields
131262
if (settings.fields.includes('area')) {
132263
const areaMap = LOADED_DATA.sub?.area
133264
if (areaMap) {
@@ -136,6 +267,7 @@ function setCityInfo(buffer: Buffer, offset: number, settings: IpLocationApiSett
136267
// offset += 1
137268
}
138269

270+
//* Process location data if locationId is available
139271
if (locationId) {
140272
let locationOffset = (locationId - 1) * settings.locationRecordSize
141273
const locationBuffer = LOADED_DATA.location
@@ -207,16 +339,30 @@ function setCityInfo(buffer: Buffer, offset: number, settings: IpLocationApiSett
207339
return setCountryInfo(geodata, settings)
208340
}
209341

342+
/**
343+
* Pads a string with leading zeros
344+
* @param text - The text to pad
345+
* @param length - The desired length of the padded string
346+
* @returns The zero-padded string
347+
*/
210348
function getZeroFill(text: string, length: number) {
211349
return '0'.repeat(length - text.length) + text
212350
}
213351

352+
/**
353+
* Adds additional country information to the GeoData object
354+
* @param geodata - The GeoData object to enhance
355+
* @param settings - The API settings
356+
* @returns A Promise that resolves to the enhanced GeoData
357+
*/
214358
async function setCountryInfo(geodata: GeoData, settings: IpLocationApiSettings): Promise<GeoData> {
215359
if (settings.addCountryInfo && geodata.country) {
216360
//* Import the countries-list package (optional peer dependency)
217361
try {
218362
const { countries, continents } = await import('countries-list')
219363
const country = countries[geodata.country as keyof typeof countries] as ICountry | undefined
364+
365+
//* Enhance geodata with additional country information
220366
geodata.country_name = country?.name
221367
geodata.country_native = country?.native
222368
geodata.continent = country?.continent ? continents[country.continent] : undefined

packages/ip-location-api/src/functions/reload.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ export const LOADED_DATA: {
1818
}
1919
} = {}
2020

21+
/**
22+
* Reloads the IP location data based on the provided settings.
23+
* @param inputSettings - Optional input settings to override the default settings.
24+
*/
2125
export async function reload(inputSettings?: IpLocationApiInputSettings): Promise<void> {
2226
const settings = getSettings(inputSettings)
2327

24-
let directoryToFind = settings.dataDir
25-
if (settings.smallMemory) {
26-
directoryToFind = join(settings.dataDir, 'v4')
27-
}
28-
29-
if (!existsSync(directoryToFind)) {
28+
//* If the data directory doesn't exist, update the database
29+
if (!existsSync(join(settings.fieldDir, '4-1.dat'))) {
3030
await update(settings)
3131
}
3232

@@ -45,11 +45,14 @@ export async function reload(inputSettings?: IpLocationApiInputSettings): Promis
4545
city: undefined as Buffer | undefined,
4646
sub: undefined as Buffer | undefined,
4747
}
48+
49+
//* Create an array of promises to read all necessary files
4850
const promises: Promise<void>[] = [
4951
readFile(join(settings.fieldDir, '4-1.dat')).then((buffer) => { buffers.v4.dat1 = buffer }),
5052
readFile(join(settings.fieldDir, '6-1.dat')).then((buffer) => { buffers.v6.dat1 = buffer }),
5153
]
5254

55+
//* Add additional file reading promises if not in small memory mode
5356
if (!settings.smallMemory) {
5457
promises.push(
5558
readFile(join(settings.fieldDir, '4-2.dat')).then((buffer) => { buffers.v4.dat2 = buffer }),
@@ -59,6 +62,7 @@ export async function reload(inputSettings?: IpLocationApiInputSettings): Promis
5962
)
6063
}
6164

65+
//* Add location-related file reading promises based on settings
6266
if (settings.locationFile) {
6367
promises.push(
6468
readFile(join(settings.fieldDir, 'location.dat')).then((buffer) => { buffers.location = buffer }),
@@ -77,14 +81,17 @@ export async function reload(inputSettings?: IpLocationApiInputSettings): Promis
7781
}
7882
}
7983

84+
//* Wait for all file reading operations to complete
8085
await Promise.all(promises)
8186

8287
const v4 = settings.v4
8388
const v6 = settings.v6
8489

90+
//* Create typed arrays from the loaded buffer data
8591
const v4StartIps = new Uint32Array(buffers.v4.dat1!.buffer, 0, buffers.v4.dat1!.byteLength >> 2)
8692
const v6StartIps = new BigUint64Array(buffers.v6.dat1!.buffer, 0, buffers.v6.dat1!.byteLength >> 3)
8793

94+
//* Set up v4 loaded data
8895
v4.loadedData = {
8996
startIps: v4StartIps,
9097
endIps: buffers.v4.dat2 ? new Uint32Array(buffers.v4.dat2.buffer, 0, buffers.v4.dat2.byteLength >> 2) : undefined,
@@ -93,6 +100,7 @@ export async function reload(inputSettings?: IpLocationApiInputSettings): Promis
93100
firstIp: v4StartIps[0]!,
94101
}
95102

103+
//* Set up v6 loaded data
96104
v6.loadedData = {
97105
startIps: v6StartIps,
98106
endIps: buffers.v6.dat2 ? new BigUint64Array(buffers.v6.dat2.buffer, 0, buffers.v6.dat2.byteLength >> 3) : undefined,
@@ -101,6 +109,7 @@ export async function reload(inputSettings?: IpLocationApiInputSettings): Promis
101109
firstIp: v6StartIps[0]!,
102110
}
103111

112+
//* Load additional data for City dataType
104113
if (settings.dataType === 'City') {
105114
LOADED_DATA.location = buffers.location
106115
LOADED_DATA.city = buffers.city
@@ -117,7 +126,10 @@ export async function reload(inputSettings?: IpLocationApiInputSettings): Promis
117126
}
118127
}
119128

120-
export function clear() {
129+
/**
130+
* Clears the loaded IP location data from memory.
131+
*/
132+
export function clear(): void {
121133
const settings = SAVED_SETTINGS
122134
settings.v4.loadedData = undefined
123135
settings.v6.loadedData = undefined

0 commit comments

Comments
 (0)