encarapi 1.0.0 → 1.1.0

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
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "encarapi",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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",
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.
@@ -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>;