apimo.js 1.0.3 → 1.0.5

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.
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Vitaly Lysen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -4,17 +4,19 @@
4
4
  [![CI](https://github.com/Neikow/apimo.js/actions/workflows/ci.yml/badge.svg)](https://github.com/Neikow/apimo.js/actions/workflows/ci.yml)
5
5
  [![codecov](https://codecov.io/gh/Neikow/apimo.js/branch/main/graph/badge.svg)](https://codecov.io/gh/Neikow/apimo.js)
6
6
 
7
- A comprehensive TypeScript wrapper for the [Apimo API](https://apimo.net/en/api/webservice/) with intelligent caching,
8
- rate limiting, and automatic catalog transformation for building custom Real Estate websites.
7
+ A TypeScript-first wrapper for the [Apimo API](https://apimo.net/en/api/webservice/) with intelligent caching, automatic retries, rate limiting, and catalog transformation for building custom real estate websites.
8
+
9
+ > **Note:** This library is in active development. No breaking changes are planned, but please pin your version in production.
9
10
 
10
11
  ## Features
11
12
 
12
- - 🚀 **TypeScript-first** - Full type safety with Zod schema validation
13
- - 📦 **Smart Caching** - Multiple cache adapters (Memory, Filesystem, Dummy) with automatic invalidation
14
- - 🔄 **Rate Limiting** - Built-in request throttling to respect API limits (1000 requests/day)
15
- - 🌍 **Multi-language Support** - Automatic catalog transformation for localized content
16
- - 🏗️ **Property & Agency APIs** - Complete coverage of Apimo's real estate endpoints
17
- - **Optimized Performance** - Intelligent catalog caching reduces API calls by 90%
13
+ - 🔷 **TypeScript-first** Full type safety with Zod schema validation on every response
14
+ - 📦 **Smart caching** Memory, Filesystem, and Dummy cache adapters with automatic TTL invalidation
15
+ - 🔁 **Automatic retries** Configurable retry strategy (exponential / linear / fixed back-off) for transient failures
16
+ - 🚦 **Rate limiting** — Built-in Bottleneck throttle to stay within Apimo's API limits
17
+ - 🌍 **Multi-language** First-class `culture` support for all endpoints and catalog lookups
18
+ - 🏗️ **Full coverage** Properties, agencies, and catalog endpoints
19
+ - 🐛 **Developer-friendly errors** — Typed error classes with status codes, request URLs, and response bodies
18
20
 
19
21
  ## Installation
20
22
 
@@ -27,153 +29,123 @@ yarn add apimo.js
27
29
  ## Quick Start
28
30
 
29
31
  ```typescript
30
- import { Apimo } from 'apimo.js'
32
+ import { Apimo, MemoryCache } from 'apimo.js'
31
33
 
32
34
  const api = new Apimo(
33
- 'YOUR_BRIDGE_ID', // Get from Apimo support
34
- 'YOUR_API_TOKEN', // Get from Apimo support
35
+ 'YOUR_PROVIDER_ID', // Numeric string — request from Apimo support
36
+ 'YOUR_API_TOKEN', // Secret token — request from Apimo support
35
37
  {
36
- culture: 'en', // Default language
38
+ culture: 'en',
37
39
  catalogs: {
38
- cache: {
39
- active: true,
40
- adapter: new MemoryCache() // or FilesystemCache()
41
- }
42
- }
43
- }
40
+ cache: { active: true, adapter: new MemoryCache() },
41
+ },
42
+ },
44
43
  )
45
44
 
46
- // Fetch properties with automatic catalog transformation
47
- const properties = await api.getProperties({
48
- limit: 10,
49
- offset: 0
50
- })
51
-
52
- // Get a specific property
53
- const property = await api.getProperty(123)
54
-
55
- // Fetch agencies
56
- const agencies = await api.getAgencies()
45
+ const { properties } = await api.fetchProperties(agencyId)
46
+ const { agencies } = await api.fetchAgencies()
57
47
  ```
58
48
 
59
49
  ## Configuration
60
50
 
61
- ### Basic Configuration
51
+ ### Full Config Reference
62
52
 
63
53
  ```typescript
64
- const api = new Apimo(bridgeId, token, {
65
- baseUrl: 'https://api.apimo.pro', // API base URL
66
- culture: 'en', // Default language (en, fr)
54
+ const api = new Apimo(providerId, token, {
55
+ // Base URL for API requests. Default: 'https://api.apimo.pro'
56
+ baseUrl: 'https://api.apimo.pro',
57
+
58
+ // Default language for all requests. Default: 'en'
59
+ culture: 'en',
60
+
67
61
  catalogs: {
68
62
  cache: {
69
- active: true, // Enable catalog caching
70
- adapter: new MemoryCache() // Cache implementation
63
+ // Enable catalog caching. Default: true
64
+ active: true,
65
+ // Cache implementation. Default: new MemoryCache()
66
+ adapter: new MemoryCache(),
71
67
  },
72
68
  transform: {
73
- active: true, // Enable catalog transformation
74
- transformFn: customTransformer // Optional custom transformer
75
- }
76
- }
69
+ // Resolve catalog IDs to human-readable names. Default: true
70
+ active: true,
71
+ // Optionally supply your own resolver function
72
+ transformFn: async (catalogName, culture, id) => { /* ... */ },
73
+ },
74
+ },
75
+
76
+ retry: {
77
+ // Total number of attempts (1 = no retries). Default: 3
78
+ attempts: 3,
79
+ // Delay in ms before the first retry. Default: 200
80
+ initialDelayMs: 200,
81
+ // Back-off strategy: 'exponential' | 'linear' | 'fixed'. Default: 'exponential'
82
+ backoff: 'exponential',
83
+ },
77
84
  })
78
85
  ```
79
86
 
80
87
  ### Cache Adapters
81
88
 
82
- #### Memory Cache (Default)
89
+ #### MemoryCache *(default)*
90
+
91
+ Stores catalog entries in process memory. Fast and zero-config. Data is lost when the process restarts.
83
92
 
84
93
  ```typescript
85
94
  import { MemoryCache } from 'apimo.js'
86
95
 
87
- const api = new Apimo(bridgeId, token, {
88
- catalogs: {
89
- cache: {
90
- adapter: new MemoryCache()
91
- }
92
- }
93
- })
96
+ const memoryCache = new MemoryCache({ cacheExpirationMs: 7 * 24 * 60 * 60 * 1000 }) // default: 1 week
94
97
  ```
95
98
 
96
- #### Filesystem Cache
99
+ #### FilesystemCache
100
+
101
+ Persists catalog entries to JSON files on disk. Survives restarts.
97
102
 
98
103
  ```typescript
99
104
  import { FilesystemCache } from 'apimo.js'
100
105
 
101
- const api = new Apimo(bridgeId, token, {
102
- catalogs: {
103
- cache: {
104
- adapter: new FilesystemCache('./cache')
105
- }
106
- }
106
+ const fsCache = new FilesystemCache({
107
+ path: './cache/catalogs', // default
108
+ cacheExpirationMs: 7 * 24 * 60 * 60 * 1000,
107
109
  })
108
110
  ```
109
111
 
110
- #### Dummy Cache (No Caching)
112
+ #### DummyCache
113
+
114
+ Disables caching entirely. Every catalog look-up hits the API directly.
111
115
 
112
116
  ```typescript
113
117
  import { DummyCache } from 'apimo.js'
114
118
 
115
- const api = new Apimo(bridgeId, token, {
116
- catalogs: {
117
- cache: {
118
- adapter: new DummyCache()
119
- }
120
- }
121
- })
119
+ const dummyCache = new DummyCache()
122
120
  ```
123
121
 
124
122
  ## API Reference
125
123
 
126
124
  ### Properties
127
125
 
128
- #### `getProperties(options?)`
129
-
130
- Fetch a list of properties with optional filtering.
126
+ #### `fetchProperties(agencyId, options?)`
131
127
 
132
128
  ```typescript
133
- const properties = await api.getProperties({
129
+ const { total_items, properties, timestamp } = await api.fetchProperties(agencyId, {
130
+ culture: 'fr',
134
131
  limit: 20,
135
132
  offset: 0,
136
- culture: 'fr',
137
- // Add property filters as needed
138
- })
139
- ```
140
-
141
- #### `getProperty(id, options?)`
142
-
143
- Fetch a specific property by ID.
144
-
145
- ```typescript
146
- const property = await api.getProperty(123, {
147
- culture: 'en'
148
133
  })
149
134
  ```
150
135
 
151
136
  ### Agencies
152
137
 
153
- #### `getAgencies(options?)`
154
-
155
- Fetch a list of agencies.
156
-
157
- ```typescript
158
- const agencies = await api.getAgencies({
159
- limit: 10,
160
- culture: 'fr'
161
- })
162
- ```
163
-
164
- #### `getAgency(id, options?)`
165
-
166
- Fetch a specific agency by ID.
138
+ #### `fetchAgencies(options?)`
167
139
 
168
140
  ```typescript
169
- const agency = await api.getAgency(456)
141
+ const { total_items, agencies } = await api.fetchAgencies({ limit: 10 })
170
142
  ```
171
143
 
172
144
  ### Catalogs
173
145
 
174
146
  #### `fetchCatalogs()`
175
147
 
176
- Get all available catalog definitions.
148
+ Returns the list of all available catalog definitions.
177
149
 
178
150
  ```typescript
179
151
  const catalogs = await api.fetchCatalogs()
@@ -181,140 +153,153 @@ const catalogs = await api.fetchCatalogs()
181
153
 
182
154
  #### `fetchCatalog(catalogName, options?)`
183
155
 
184
- Fetch entries for a specific catalog.
156
+ Fetches raw entries for a specific catalog.
185
157
 
186
158
  ```typescript
187
- const propertyTypes = await api.fetchCatalog('property_type', {
188
- culture: 'en'
189
- })
159
+ const entries = await api.fetchCatalog('property_type', { culture: 'en' })
190
160
  ```
191
161
 
192
162
  #### `getCatalogEntries(catalogName, options?)`
193
163
 
194
- Get catalog entries from cache (populates cache if needed).
164
+ Returns catalog entries, automatically populating the cache when empty or expired.
195
165
 
196
166
  ```typescript
197
- const entries = await api.getCatalogEntries('property_category', {
198
- culture: 'fr'
199
- })
167
+ const entries = await api.getCatalogEntries('property_category', { culture: 'fr' })
200
168
  ```
201
169
 
202
170
  ### Low-level API
203
171
 
204
172
  #### `get(path, schema, options?)`
205
173
 
206
- Make a direct API call with schema validation.
174
+ Makes a direct API call with Zod schema validation. Retries are applied automatically.
207
175
 
208
176
  ```typescript
209
177
  import { z } from 'zod'
210
178
 
211
- const customSchema = z.object({
212
- id: z.number(),
213
- name: z.string()
214
- })
215
-
216
- const result = await api.get(['custom', 'endpoint'], customSchema, {
217
- culture: 'en',
218
- limit: 10
219
- })
179
+ const result = await api.get(
180
+ ['agencies', '123', 'properties'],
181
+ z.object({ total_items: z.number() }),
182
+ { culture: 'en', limit: 10 },
183
+ )
220
184
  ```
221
185
 
222
186
  #### `fetch(...args)`
223
187
 
224
- Direct fetch with automatic authentication headers.
225
-
226
- ```typescript
227
- const response = await api.fetch('https://api.apimo.pro/properties')
228
- ```
229
-
230
- ## Data Transformation
231
-
232
- Apimo.js automatically transforms catalog IDs into human-readable names:
188
+ Raw authenticated fetch adds the `Authorization` header and runs through the rate limiter. Use when you need full control.
233
189
 
234
190
  ```typescript
235
- // Raw API response
236
- const rawResponse = {
237
- type: 1,
238
- category: 2
239
- }
240
-
241
- // Transformed response
242
- const transformedResponse = {
243
- type: 'House',
244
- category: 'Sale'
245
- }
191
+ const response = await api.fetch('https://api.apimo.pro/catalogs')
246
192
  ```
247
193
 
248
- ### Custom Transformers
194
+ ## Error Handling
249
195
 
250
- ```typescript
251
- async function customTransformer(catalogName, culture, id) {
252
- // Your custom transformation logic
253
- return await myCustomCatalogLookup(catalogName, culture, id)
254
- }
196
+ All errors thrown by the library extend `ApimoError`, so you can catch them selectively.
255
197
 
256
- const api = new Apimo(bridgeId, token, {
257
- catalogs: {
258
- transform: {
259
- active: true,
260
- transformFn: customTransformer
261
- }
262
- }
263
- })
264
- ```
198
+ ### Error Hierarchy
265
199
 
266
- ## Rate Limiting
200
+ | Class | When thrown |
201
+ |---|---|
202
+ | `ApimoError` | Base class — catch-all |
203
+ | `ApiHttpError` | Any non-2xx HTTP response — has `.statusCode`, `.url`, `.responseBody` |
204
+ | `ApiBadRequestError` | HTTP 400 — malformed request |
205
+ | `ApiUnauthorizedError` | HTTP 401 — invalid credentials |
206
+ | `ApiForbiddenError` | HTTP 403 — insufficient permissions |
207
+ | `ApiNotFoundError` | HTTP 404 — resource does not exist |
208
+ | `ApiRateLimitError` | HTTP 429 — rate limit exceeded |
209
+ | `ApiServerError` | HTTP 5xx — Apimo server-side failure |
210
+ | `ApiResponseValidationError` | Response body doesn't match expected schema — has `.url`, `.zodError` |
211
+ | `ApiConfigurationError` | Invalid constructor arguments (empty credentials, bad `baseUrl`) |
212
+ | `ApiRetryExhaustedError` | All retry attempts failed — has `.attempts`, `.cause` |
267
213
 
268
- The library includes built-in rate limiting to respect Apimo's API limits:
214
+ ### Retry behaviour
269
215
 
270
- - **10 requests per second** (configurable)
271
- - **Automatic queuing** of excess requests
272
- - **Bottleneck integration** for advanced rate limiting scenarios
216
+ Retries are applied automatically to **transient** errors (429, 5xx, network failures). **Non-transient** errors (4xx except 429, schema validation failures) are thrown immediately without retrying.
273
217
 
274
- ## Error Handling
218
+ When all attempts are exhausted, `ApiRetryExhaustedError` is thrown. Its `.cause` property holds the underlying error from the last attempt.
275
219
 
276
220
  ```typescript
221
+ import {
222
+ ApimoError,
223
+ ApiResponseValidationError,
224
+ ApiRetryExhaustedError,
225
+ ApiServerError,
226
+ ApiUnauthorizedError,
227
+ CacheExpiredError,
228
+ } from 'apimo.js'
229
+
277
230
  try {
278
- const properties = await api.getProperties()
231
+ const { properties } = await api.fetchProperties(agencyId)
279
232
  }
280
233
  catch (error) {
281
- if (error instanceof CacheExpiredError) {
282
- // Handle cache expiration
234
+ if (error instanceof ApiUnauthorizedError) {
235
+ // Credentials are wrong — fail fast, no retry
236
+ console.error('Check your provider ID and token.')
237
+ }
238
+ else if (error instanceof ApiRetryExhaustedError) {
239
+ // Transient failure survived all retries
240
+ console.error(`Failed after ${error.attempts} attempts.`, error.cause)
283
241
  }
284
- else {
285
- // Handle other API errors
286
- console.error('API Error:', error.message)
242
+ else if (error instanceof ApiResponseValidationError) {
243
+ // The API changed its response shape
244
+ console.error('Schema mismatch:', error.zodError.format())
245
+ }
246
+ else if (error instanceof ApimoError) {
247
+ // Any other library error
248
+ console.error(error.message)
287
249
  }
288
250
  }
289
251
  ```
290
252
 
291
- ## TypeScript Support
253
+ ### Disabling retries
254
+
255
+ ```typescript
256
+ const api = new Apimo(providerId, token, {
257
+ retry: { attempts: 1 }, // 1 attempt = no retries
258
+ })
259
+ ```
260
+
261
+ ## Data Transformation
292
262
 
293
- Full TypeScript support with comprehensive type definitions:
263
+ When `catalogs.transform.active` is `true` (the default), numeric catalog IDs in API responses are automatically resolved to their human-readable names via the cache.
294
264
 
295
265
  ```typescript
296
- import type { Agency, CatalogEntry, Property } from 'apimo.js'
266
+ // Without transformation: { type: 1, category: 6 }
267
+ // With transformation: { type: 'House', category: 'Sale' }
268
+ ```
269
+
270
+ ### Custom transformer
297
271
 
298
- const property: Property = await api.getProperty(123)
299
- const agency: Agency = await api.getAgency(456)
272
+ ```typescript
273
+ const api = new Apimo(providerId, token, {
274
+ catalogs: {
275
+ transform: {
276
+ active: true,
277
+ transformFn: async (catalogName, culture, id) => {
278
+ return await myDatabase.lookupCatalog(catalogName, culture, id)
279
+ },
280
+ },
281
+ },
282
+ })
300
283
  ```
301
284
 
302
- ## Supported Languages
285
+ ## Rate Limiting
303
286
 
304
- - English (`en`)
305
- - French (`fr`)
287
+ The library uses [Bottleneck](https://github.com/SGrondin/bottleneck) internally to throttle requests to **10 per second** — well within Apimo's documented daily limits. No extra configuration is required.
306
288
 
307
- More languages can be added based on Apimo API support.
289
+ ## TypeScript
308
290
 
309
- ## Contributing
291
+ All public methods and types are fully typed. Import what you need:
310
292
 
311
- 1. Fork the repository
312
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
313
- 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
314
- 4. Push to the branch (`git push origin feature/amazing-feature`)
315
- 5. Open a Pull Request
293
+ ```typescript
294
+ import type {
295
+ AdditionalConfig,
296
+ ApimoAgency,
297
+ ApimoProperty,
298
+ CatalogEntry,
299
+ } from 'apimo.js'
300
+ ```
316
301
 
317
- ### Development
302
+ ## Development
318
303
 
319
304
  ```bash
320
305
  # Install dependencies
@@ -326,25 +311,26 @@ yarn test
326
311
  # Run tests with coverage
327
312
  yarn test-coverage
328
313
 
329
- # Run linting
314
+ # Lint
330
315
  yarn lint
331
316
 
332
- # Build the package
317
+ # Build
333
318
  yarn build
334
319
  ```
335
320
 
336
- ## License
321
+ ## Getting API Credentials
337
322
 
338
- MIT License - see the [LICENSE](LICENSE) file for details.
323
+ You need two things to use this library:
339
324
 
340
- ## Getting API Credentials
325
+ 1. **Provider ID** — a numeric string identifying your site
326
+ 2. **API Token** — your secret authentication token
341
327
 
342
- To use this library, you need:
328
+ Request both by contacting [Apimo customer service](https://apimo.net/en/contact/).
343
329
 
344
- 1. **Bridge ID** - Your site identifier (string of numbers)
345
- 2. **API Token** - Secret token for authentication
330
+ ## Roadmap
346
331
 
347
- Contact [Apimo customer service](https://apimo.net/en/contact/) to request your credentials.
332
+ - [ ] Minified production build
333
+ - [ ] Complete JSDoc coverage for all public methods
348
334
 
349
335
  ## Support
350
336
 
@@ -352,6 +338,10 @@ Contact [Apimo customer service](https://apimo.net/en/contact/) to request your
352
338
  - 🐛 [Issue Tracker](https://github.com/Neikow/apimo.js/issues)
353
339
  - 💬 [Discussions](https://github.com/Neikow/apimo.js/discussions)
354
340
 
341
+ ## License
342
+
343
+ MIT — see [LICENSE](LICENSE) for details.
344
+
355
345
  ---
356
346
 
357
347
  Made with ❤️ by [Vitaly Lysen](https://lysen.dev)