encarapi 1.0.0 → 1.1.1

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/README.md CHANGED
@@ -1,19 +1,19 @@
1
1
  # EnCarAPI: Node.js client for Korean and Chinese used car data
2
2
 
3
- Official **Node.js / TypeScript client** for [EnCarAPI](https://encarapi.com), the REST
3
+ Official **Node.js / TypeScript client** for [EnCarAPI](https://encarapi.com/?utm_source=readme&utm_medium=encarapi-node), the REST
4
4
  **Encar API** and **Korean Car API**. One package for:
5
5
 
6
6
  - **Korea:** Encar.com, KB Chachacha (`source: "kbc"`) and K Car (`source: "kcar"`), with
7
7
  photos, specs, options, inspection reports, accident and ownership records, condition
8
8
  reports, price history, a change feed and full catalog export.
9
- - **China:** Dongchedi and Che168 in English via [ChinaCarAPI](https://chinacarapi.com)
9
+ - **China:** Dongchedi and Che168 in English via [ChinaCarAPI](https://chinacarapi.com/?utm_source=readme&utm_medium=encarapi-node)
10
10
  (separate key or the China add-on for EnCarAPI keys).
11
11
 
12
12
  Built for car exporters, dealers and marketplaces that need reliable used car data without
13
13
  scraping, proxies or geo-blocks.
14
14
 
15
15
  > **An API key is required.** The data is a paid service. Get a key (5-day trial) at
16
- > **[encarapi.com](https://encarapi.com)**.
16
+ > **[encarapi.com](https://encarapi.com/?utm_source=readme&utm_medium=encarapi-node)**.
17
17
 
18
18
  ## Install
19
19
 
@@ -66,6 +66,16 @@ for await (const change of client.korea.iterateChanges({ cursor: savedCursor ??
66
66
  saveCursor(client.korea.lastCursor);
67
67
  ```
68
68
 
69
+ The China API works the same way (`client.china`); its events are
70
+ `{ id, source, vehicleId, type: "new" | "price" | "removed" | "relisted", oldPrice, newPrice, at }`:
71
+
72
+ ```js
73
+ for await (const change of client.china.iterateChanges({ cursor: savedCursor ?? 0 })) {
74
+ // change.type: "new" | "price" | "removed" | "relisted"
75
+ }
76
+ saveCursor(client.china.lastCursor);
77
+ ```
78
+
69
79
  ## Korea API (`client.korea`)
70
80
 
71
81
  | Method | Endpoint | Notes |
@@ -92,10 +102,11 @@ saveCursor(client.korea.lastCursor);
92
102
  | Method | Endpoint | Notes |
93
103
  |---|---|---|
94
104
  | `catalog(params)` | `GET /api/catalog` | `make`, `price_min/max` (CNY), `export_ready`, `sort`, ... |
105
+ | `iterateCatalog(params)` | `GET /api/catalog` | Async iterator over all pages (up to the 10,000-result depth limit) |
95
106
  | `vehicle(id)` | `GET /api/vehicle/:id` | Price history, photos, seller, export status |
96
107
  | `inspection(id)` | `GET /api/inspection/:id` | Accident, flood and fire checks, EV battery data |
97
108
  | `bulkVehicles(ids)` | `POST /api/vehicle/bulk` | Up to 500 ids |
98
- | `changes(params)` | `GET /api/catalog/changes` | Change feed |
109
+ | `changes(params)` / `iterateChanges(params)` | `GET /api/catalog/changes` | Change feed (Business/Scale) |
99
110
  | `exportCsv()` | `GET /api/catalog/export` | Full catalog CSV |
100
111
  | `enums()` / `models(make)` | `GET /api/enums`, `/api/models` | Filter values, models per make |
101
112
 
@@ -117,10 +128,10 @@ try {
117
128
 
118
129
  ## Links
119
130
 
120
- - Website and pricing: https://encarapi.com
121
- - Documentation: https://encarapi.com/documentation
131
+ - Website and pricing: [encarapi.com](https://encarapi.com/?utm_source=readme&utm_medium=encarapi-node)
132
+ - Documentation: [encarapi.com/documentation](https://encarapi.com/documentation?utm_source=readme&utm_medium=encarapi-node)
122
133
  - OpenAPI reference: https://api.encarapi.com/reference
123
- - China data: https://chinacarapi.com
134
+ - China data: [chinacarapi.com](https://chinacarapi.com/?utm_source=readme&utm_medium=encarapi-node)
124
135
  - Python client: https://github.com/ThatMojo/encarapi-python
125
136
 
126
137
  EnCarAPI is an independent service and not affiliated with Encar, KB Chachacha, K Car,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "encarapi",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Official Node.js client for EnCarAPI: Korean used car data (Encar, KB Chachacha, K Car) and Chinese used car data (Dongchedi, Che168) in one REST API.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -36,7 +36,7 @@
36
36
  "car export",
37
37
  "vehicle data api"
38
38
  ],
39
- "homepage": "https://encarapi.com",
39
+ "homepage": "https://encarapi.com/?utm_source=npm&utm_medium=encarapi-node",
40
40
  "repository": {
41
41
  "type": "git",
42
42
  "url": "git+https://github.com/ThatMojo/encarapi-node.git"
package/src/china.js CHANGED
@@ -4,6 +4,9 @@ const { HttpClient } = require("./http");
4
4
 
5
5
  const enc = encodeURIComponent;
6
6
 
7
+ // /api/catalog serves at most this many results per query (page * limit).
8
+ const CATALOG_DEPTH = 10000;
9
+
7
10
  /**
8
11
  * Chinese used-car data (Dongchedi and Che168) in English. Works with a
9
12
  * ChinaCarAPI key or an EnCarAPI key that has the China add-on.
@@ -13,7 +16,7 @@ class ChinaClient {
13
16
  this.http = new HttpClient({
14
17
  apiKey,
15
18
  baseUrl: options.baseUrl || "https://api.chinacarapi.com",
16
- signupUrl: "https://chinacarapi.com",
19
+ signupUrl: "https://chinacarapi.com/?utm_source=sdk&utm_medium=encarapi-node&utm_content=error#pricing",
17
20
  product: "ChinaCarAPI",
18
21
  fetchImpl: options.fetch,
19
22
  });
@@ -22,6 +25,21 @@ class ChinaClient {
22
25
  /** Search listings. Filters: source, make, model, year_min/max, price_min/max (CNY), mileage_max, city, fuel, has_report, export_ready, sort, page, limit, lang. */
23
26
  catalog(params) { return this.http.request("GET", "/api/catalog", { params }); }
24
27
 
28
+ /**
29
+ * Async iterator over all catalog pages: for await (const car of china.iterateCatalog({...})) {}
30
+ * Stops at the API's 10,000-result depth limit; for the whole catalog use
31
+ * exportCsv() plus iterateChanges().
32
+ */
33
+ async *iterateCatalog(params = {}) {
34
+ const limit = Math.min(params.limit || 100, 100);
35
+ for (let page = params.page || 1; ; page++) {
36
+ const res = await this.catalog({ ...params, limit, page });
37
+ const items = res.results || [];
38
+ for (const item of items) yield item;
39
+ if (items.length < limit || (page + 1) * limit > CATALOG_DEPTH) return;
40
+ }
41
+ }
42
+
25
43
  /** Full record for one car: price history, photos, seller, export status, alsoListedOn. */
26
44
  vehicle(id, params) { return this.http.request("GET", `/api/vehicle/${enc(id)}`, { params }); }
27
45
 
@@ -37,6 +55,23 @@ class ChinaClient {
37
55
  /** Change feed: { since } once, then { cursor: nextCursor } (Business/Scale). */
38
56
  changes(params) { return this.http.request("GET", "/api/catalog/changes", { params }); }
39
57
 
58
+ /**
59
+ * Follows the change feed until it is drained and yields every event once:
60
+ * { id, source, vehicleId, type: "new" | "price" | "removed" | "relisted", oldPrice, newPrice, at }.
61
+ * Pass { since } (right after an export) or { cursor } to start; after the loop,
62
+ * `china.lastCursor` holds the cursor to resume from next time.
63
+ */
64
+ async *iterateChanges(params = {}) {
65
+ let query = { ...params };
66
+ for (;;) {
67
+ const res = await this.changes(query);
68
+ for (const item of res.changes || []) yield item;
69
+ if (res.nextCursor !== undefined) this.lastCursor = res.nextCursor;
70
+ if (!res.hasMore || res.nextCursor === undefined) return;
71
+ query = { ...params, since: undefined, cursor: res.nextCursor };
72
+ }
73
+ }
74
+
40
75
  /** Full catalog as CSV text (Business/Scale). */
41
76
  exportCsv(params) { return this.http.request("GET", "/api/catalog/export", { params, text: true }); }
42
77
 
package/src/index.d.ts CHANGED
@@ -169,16 +169,45 @@ export interface ChinaCatalogResponse {
169
169
  results: Json[];
170
170
  }
171
171
 
172
+ export interface ChinaChangesParams {
173
+ /** ISO timestamp for the first call. */
174
+ since?: string;
175
+ /** nextCursor from the previous call. */
176
+ cursor?: number;
177
+ source?: "dongchedi" | "che168" | (string & {});
178
+ limit?: number;
179
+ }
180
+
181
+ export interface ChinaChange extends Json {
182
+ id: number;
183
+ source: string;
184
+ vehicleId: string;
185
+ type: "new" | "price" | "removed" | "relisted";
186
+ at: string;
187
+ }
188
+
189
+ export interface ChinaChangesResponse {
190
+ cursor: number;
191
+ nextCursor: number;
192
+ hasMore: boolean;
193
+ changes: ChinaChange[];
194
+ }
195
+
172
196
  /** Chinese used-car data: Dongchedi and Che168. */
173
197
  export class ChinaClient {
174
198
  constructor(apiKey: string, options?: Pick<ClientOptions, "fetch"> & { baseUrl?: string });
199
+ /** Cursor to resume from after iterateChanges() finished. */
200
+ lastCursor?: number;
175
201
  catalog(params?: ChinaCatalogParams): Promise<ChinaCatalogResponse>;
176
- vehicle(id: string, params?: { lang?: "en" | "zh" }): Promise<Json>;
177
- inspection(id: string, params?: { lang?: "en" | "zh" }): Promise<Json>;
202
+ /** All pages up to the API's 10,000-result depth limit. */
203
+ iterateCatalog(params?: ChinaCatalogParams): AsyncGenerator<Json, void, unknown>;
204
+ vehicle(id: string, params?: { source?: "dongchedi" | "che168"; lang?: "en" | "zh" }): Promise<Json>;
205
+ inspection(id: string, params?: { source?: "dongchedi" | "che168"; lang?: "en" | "zh" }): Promise<Json>;
178
206
  bulkVehicles(ids: string[], params?: { lang?: "en" | "zh" }): Promise<Json>;
179
207
  /** Alias of bulkVehicles. */
180
208
  bulk(ids: string[], params?: { lang?: "en" | "zh" }): Promise<Json>;
181
- changes(params: { since?: string; cursor?: number; source?: string; limit?: number }): Promise<Json>;
209
+ changes(params: ChinaChangesParams): Promise<ChinaChangesResponse>;
210
+ iterateChanges(params: ChinaChangesParams): AsyncGenerator<ChinaChange, void, unknown>;
182
211
  exportCsv(params?: Json): Promise<string>;
183
212
  enums(params?: Json): Promise<Json>;
184
213
  models(make: string | number): Promise<Json>;
package/src/index.js CHANGED
@@ -23,7 +23,7 @@ class EnCarAPI {
23
23
  if (!apiKey && !chinaKey) {
24
24
  throw new EnCarAPIError(
25
25
  "An API key is required. Pass new EnCarAPI('YOUR_KEY') or set ENCARAPI_KEY " +
26
- "(Korea) / CHINACARAPI_KEY (China). Get a key at https://encarapi.com"
26
+ "(Korea) / CHINACARAPI_KEY (China). Get a key at https://encarapi.com/?utm_source=sdk&utm_medium=encarapi-node&utm_content=error"
27
27
  );
28
28
  }
29
29
  this._apiKey = apiKey;
@@ -35,7 +35,7 @@ class EnCarAPI {
35
35
  get korea() {
36
36
  if (!this._korea) {
37
37
  if (!this._apiKey) {
38
- throw new EnCarAPIError("An EnCarAPI key is required for Korean data. Get one at https://encarapi.com");
38
+ throw new EnCarAPIError("An EnCarAPI key is required for Korean data. Get one at https://encarapi.com/?utm_source=sdk&utm_medium=encarapi-node&utm_content=error");
39
39
  }
40
40
  this._korea = new KoreaClient(this._apiKey, { baseUrl: this._options.baseUrl, fetch: this._options.fetch });
41
41
  }
@@ -68,7 +68,7 @@ class ChinaCarAPI extends ChinaClient {
68
68
  if (!apiKey) {
69
69
  throw new EnCarAPIError(
70
70
  "A ChinaCarAPI key is required. Pass new ChinaCarAPI('YOUR_KEY') or set CHINACARAPI_KEY. " +
71
- "Get a key at https://chinacarapi.com"
71
+ "Get a key at https://chinacarapi.com/?utm_source=sdk&utm_medium=encarapi-node&utm_content=error"
72
72
  );
73
73
  }
74
74
  super(apiKey, options);
package/src/korea.js CHANGED
@@ -15,7 +15,7 @@ class KoreaClient {
15
15
  this.http = new HttpClient({
16
16
  apiKey,
17
17
  baseUrl: options.baseUrl || "https://api.encarapi.com",
18
- signupUrl: "https://encarapi.com",
18
+ signupUrl: "https://encarapi.com/?utm_source=sdk&utm_medium=encarapi-node&utm_content=error#pricing",
19
19
  product: "EnCarAPI",
20
20
  fetchImpl: options.fetch,
21
21
  });