catastrogps 1.0.0 → 1.2.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/dist/index.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ADDRESS_COUNTRY_CODES: () => ADDRESS_COUNTRY_CODES,
23
24
  AmbiguousReferenceError: () => AmbiguousReferenceError,
24
25
  AuthenticationError: () => AuthenticationError,
25
26
  COUNTRY_CODES: () => COUNTRY_CODES,
@@ -33,13 +34,16 @@ __export(index_exports, {
33
34
  ForbiddenError: () => ForbiddenError,
34
35
  NetworkError: () => NetworkError,
35
36
  NotFoundError: () => NotFoundError,
37
+ PlaceNameError: () => PlaceNameError,
36
38
  QuotaExceededError: () => QuotaExceededError,
37
39
  RateLimitError: () => RateLimitError,
38
40
  ServerError: () => ServerError,
39
41
  ServiceUnavailableError: () => ServiceUnavailableError,
40
42
  TimeoutError: () => TimeoutError,
43
+ UNITS_COUNTRY_CODES: () => UNITS_COUNTRY_CODES,
41
44
  VERSION: () => VERSION,
42
- ValidationError: () => ValidationError
45
+ ValidationError: () => ValidationError,
46
+ readQuota: () => readQuota
43
47
  });
44
48
  module.exports = __toCommonJS(index_exports);
45
49
 
@@ -70,8 +74,24 @@ var NotFoundError = class extends CatastroGPSError {
70
74
  var ValidationError = class extends CatastroGPSError {
71
75
  name = "ValidationError";
72
76
  };
77
+ var PlaceNameError = class extends ValidationError {
78
+ name = "PlaceNameError";
79
+ get location() {
80
+ const details = this.details;
81
+ return details?.location;
82
+ }
83
+ };
73
84
  var CoverageError = class extends CatastroGPSError {
74
85
  name = "CoverageError";
86
+ get country() {
87
+ return this.details?.country;
88
+ }
89
+ get feature() {
90
+ return this.details?.feature;
91
+ }
92
+ get supportedCountries() {
93
+ return this.details?.supportedCountries ?? [];
94
+ }
75
95
  };
76
96
  var AmbiguousReferenceError = class extends CatastroGPSError {
77
97
  name = "AmbiguousReferenceError";
@@ -82,6 +102,10 @@ var AmbiguousReferenceError = class extends CatastroGPSError {
82
102
  };
83
103
  var QuotaExceededError = class extends CatastroGPSError {
84
104
  name = "QuotaExceededError";
105
+ quota;
106
+ get prepaidBalanceEmpty() {
107
+ return this.quota?.overage?.enabled === true;
108
+ }
85
109
  };
86
110
  var RateLimitError = class extends CatastroGPSError {
87
111
  name = "RateLimitError";
@@ -89,6 +113,7 @@ var RateLimitError = class extends CatastroGPSError {
89
113
  };
90
114
  var ServiceUnavailableError = class extends CatastroGPSError {
91
115
  name = "ServiceUnavailableError";
116
+ retryAfterSeconds;
92
117
  };
93
118
  var ServerError = class extends CatastroGPSError {
94
119
  name = "ServerError";
@@ -99,14 +124,19 @@ var TimeoutError = class extends CatastroGPSError {
99
124
  var NetworkError = class extends CatastroGPSError {
100
125
  name = "NetworkError";
101
126
  };
102
- function errorFromResponse(status, body, retryAfter) {
127
+ function errorFromResponse(status, body, retryAfter, quota) {
103
128
  const code = typeof body.code === "string" ? body.code : void 0;
104
129
  const message = typeof body.error === "string" && body.error || typeof body.message === "string" && body.message || `HTTP ${status}`;
105
130
  const details = body.data ?? body.parsed;
106
131
  const options = { status, code, details, body };
107
132
  if (code === "CNV_AMBIGUOUS" || status === 300) return new AmbiguousReferenceError(message, options);
108
133
  if (code === "CNV_COVERAGE") return new CoverageError(message, options);
109
- if (code === "KEY_AUTH_004") return new QuotaExceededError(message, options);
134
+ if (code === "CNV_PLACE_NAME") return new PlaceNameError(message, options);
135
+ if (code === "KEY_AUTH_004") {
136
+ const error = new QuotaExceededError(message, options);
137
+ error.quota = quota;
138
+ return error;
139
+ }
110
140
  if (status === 429) {
111
141
  const error = new RateLimitError(message, options);
112
142
  error.retryAfterSeconds = retryAfter;
@@ -116,7 +146,11 @@ function errorFromResponse(status, body, retryAfter) {
116
146
  if (status === 403) return new ForbiddenError(message, options);
117
147
  if (status === 404) return new NotFoundError(message, options);
118
148
  if (status === 400 || status === 422) return new ValidationError(message, options);
119
- if (status === 503) return new ServiceUnavailableError(message, options);
149
+ if (status === 503) {
150
+ const error = new ServiceUnavailableError(message, options);
151
+ error.retryAfterSeconds = retryAfter;
152
+ return error;
153
+ }
120
154
  if (status >= 500) return new ServerError(message, options);
121
155
  return new CatastroGPSError(message, options);
122
156
  }
@@ -140,7 +174,75 @@ var ExportResource = class {
140
174
  }
141
175
  };
142
176
 
177
+ // src/types.ts
178
+ var COUNTRY_CODES = [
179
+ "ES",
180
+ "PV",
181
+ "NA",
182
+ "PT",
183
+ "FR",
184
+ "IT",
185
+ "DE",
186
+ "AT",
187
+ "CH",
188
+ "LI",
189
+ "BE",
190
+ "NL",
191
+ "LU",
192
+ "PL",
193
+ "CZ",
194
+ "SK",
195
+ "SI",
196
+ "HR",
197
+ "BG",
198
+ "GR",
199
+ "CY",
200
+ "DK",
201
+ "SE",
202
+ "NO",
203
+ "FI",
204
+ "IS",
205
+ "EE",
206
+ "LV",
207
+ "LT",
208
+ "IE",
209
+ "UK"
210
+ ];
211
+ var ENRICHMENT_COUNTRY_CODES = ["ES", "PV", "NA", "PT", "FR", "IT", "DE"];
212
+ var ADDRESS_COUNTRY_CODES = [
213
+ "ES",
214
+ "FR",
215
+ "IT",
216
+ "DE",
217
+ "AT",
218
+ "NL",
219
+ "BE",
220
+ "PL",
221
+ "CH",
222
+ "CZ",
223
+ "DK",
224
+ "NO",
225
+ "FI",
226
+ "EE",
227
+ "LV",
228
+ "LT",
229
+ "SI",
230
+ "SK",
231
+ "BG",
232
+ "GR",
233
+ "CY",
234
+ "LU",
235
+ "LI",
236
+ "IS",
237
+ "IE",
238
+ "UK",
239
+ "PT"
240
+ ];
241
+ var UNITS_COUNTRY_CODES = ["ES", "PV", "NA"];
242
+
143
243
  // src/resources/parcels.ts
244
+ var MIN_COMPARE = 2;
245
+ var MAX_COMPARE = 3;
144
246
  var ParcelsResource = class {
145
247
  constructor(client) {
146
248
  this.client = client;
@@ -173,14 +275,119 @@ var ParcelsResource = class {
173
275
  });
174
276
  return data.agro ?? data;
175
277
  }
278
+ async terrain(reference, options = {}) {
279
+ return this.client.request("GET", parcelPath(reference, "/terrain"), { query: locatedQuery(options) });
280
+ }
281
+ async groundMotion(reference, options = {}) {
282
+ return this.client.request("GET", parcelPath(reference, "/ground-motion"), { query: locatedQuery(options) });
283
+ }
284
+ async market(reference, options = {}) {
285
+ return this.client.request("GET", parcelPath(reference, "/market"), { query: { country: options.country } });
286
+ }
287
+ async score(reference, options = {}) {
288
+ return this.client.request("GET", parcelPath(reference, "/score"), { query: { country: options.country } });
289
+ }
290
+ async valueHistory(reference, options = {}) {
291
+ return this.client.request("GET", parcelPath(reference, "/value-history"), {
292
+ query: { country: options.country }
293
+ });
294
+ }
295
+ async compare(parcels) {
296
+ if (parcels.length < MIN_COMPARE || parcels.length > MAX_COMPARE) {
297
+ throw new CatastroGPSError(`compare takes ${MIN_COMPARE} or ${MAX_COMPARE} parcels`);
298
+ }
299
+ const parcelas = parcels.map((parcel) => {
300
+ if (!parcel.reference?.trim()) throw new CatastroGPSError("reference must not be empty");
301
+ if (!ENRICHMENT_COUNTRY_CODES.includes(parcel.country)) {
302
+ throw new CatastroGPSError(`country must be one of ${ENRICHMENT_COUNTRY_CODES.join(", ")}`);
303
+ }
304
+ return { ref_catastral: parcel.reference.trim(), country: parcel.country };
305
+ });
306
+ return this.client.request("POST", "/api/catastro/compare", { body: { parcelas } });
307
+ }
176
308
  };
309
+ function locatedQuery(options) {
310
+ const hasLat = options.lat !== void 0;
311
+ const hasLng = options.lng !== void 0;
312
+ if (hasLat !== hasLng) throw new CatastroGPSError("lat and lng go together");
313
+ if (hasLat && (!Number.isFinite(options.lat) || !Number.isFinite(options.lng))) {
314
+ throw new CatastroGPSError("lat and lng must be finite numbers");
315
+ }
316
+ return { country: options.country, lat: options.lat, lng: options.lng };
317
+ }
177
318
  function parcelPath(reference, suffix = "") {
178
319
  if (!reference.trim()) throw new CatastroGPSError("reference must not be empty");
179
320
  return `/api/catastro/${encodeURIComponent(reference.trim())}${suffix}`;
180
321
  }
181
322
 
323
+ // src/resources/fincas.ts
324
+ var MAX_PAGES = 1e3;
325
+ var FincasResource = class {
326
+ constructor(client) {
327
+ this.client = client;
328
+ }
329
+ client;
330
+ async searchAddress(query) {
331
+ const params = typeof query === "string" ? { q: query } : query;
332
+ if (!params.q?.trim() && !params.street?.trim()) throw new CatastroGPSError("q or street is required");
333
+ return this.client.request("GET", "/api/search/address/candidates", {
334
+ query: {
335
+ country: params.country,
336
+ q: params.q?.trim(),
337
+ street: params.street?.trim(),
338
+ number: params.number,
339
+ municipality: params.municipality?.trim(),
340
+ postcode: params.postcode?.trim(),
341
+ limit: params.limit
342
+ }
343
+ });
344
+ }
345
+ async *pages(refcat14, options = {}) {
346
+ const path = parcelPath(refcat14, "/units");
347
+ const previousByCursor = new Map((options.previous?.pages ?? []).map((page) => [page.cursor ?? "", page]));
348
+ let cursor;
349
+ for (let count = 0; count < MAX_PAGES; count += 1) {
350
+ const previous = previousByCursor.get(cursor ?? "");
351
+ const response = await this.client.conditionalRequest("GET", path, {
352
+ query: { cursor, country: options.country },
353
+ ifNoneMatch: previous?.etag
354
+ });
355
+ const data = response.notModified && previous ? previous.data : response.data;
356
+ if (!data) throw new CatastroGPSError("The API answered 304 for a page that was not in the previous import");
357
+ const page = { cursor, etag: response.etag, notModified: response.notModified, data };
358
+ yield page;
359
+ if (!data.truncated || !data.nextCursor) return;
360
+ cursor = data.nextCursor;
361
+ }
362
+ throw new CatastroGPSError(`Stopped after ${MAX_PAGES} pages`);
363
+ }
364
+ async units(refcat14, options = {}) {
365
+ const pages = [];
366
+ for await (const page of this.pages(refcat14, options)) pages.push(page);
367
+ const first = pages[0].data;
368
+ const last = pages[pages.length - 1].data;
369
+ return {
370
+ refCatastral: first.refCatastral,
371
+ direccion: first.direccion,
372
+ codigoPostal: first.codigoPostal,
373
+ municipio: first.municipio,
374
+ provincia: first.provincia,
375
+ usoGeneral: first.usoGeneral,
376
+ anioConstruccion: first.anioConstruccion,
377
+ totalUnidadesFinca: last.totalUnidadesFinca ?? pages.reduce((sum, p) => sum + p.data.unidades.length, 0),
378
+ unidades: pages.flatMap((page) => page.data.unidades),
379
+ construcciones: pages.flatMap((page) => page.data.construcciones ?? []),
380
+ dataSource: first.dataSource,
381
+ dataDate: first.dataDate,
382
+ attribution: first.attribution,
383
+ changed: !pages.every((page) => page.notModified) || pages.length !== (options.previous?.pages.length ?? -1),
384
+ pages
385
+ };
386
+ }
387
+ };
388
+
182
389
  // src/version.ts
183
- var VERSION = "1.0.0";
390
+ var VERSION = "1.2.0";
184
391
 
185
392
  // src/client.ts
186
393
  var DEFAULT_BASE_URL = "https://api.catastrogps.es";
@@ -188,9 +395,12 @@ var DEFAULT_TIMEOUT_MS = 3e4;
188
395
  var DEFAULT_MAX_RETRIES = 2;
189
396
  var BACKOFF_BASE_MS = 500;
190
397
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([502, 503, 504]);
398
+ var NOT_MODIFIED = 304;
399
+ var MAX_RETRY_AFTER_SECONDS = 60;
191
400
  var CatastroGPS = class {
192
401
  parcels;
193
402
  export;
403
+ fincas;
194
404
  lastQuota;
195
405
  apiKey;
196
406
  baseUrl;
@@ -215,11 +425,25 @@ var CatastroGPS = class {
215
425
  this.fetchImpl = fetchImpl;
216
426
  this.parcels = new ParcelsResource(this);
217
427
  this.export = new ExportResource(this);
428
+ this.fincas = new FincasResource(this);
429
+ }
430
+ async searchAddressCandidates(query) {
431
+ return this.fincas.searchAddress(query);
432
+ }
433
+ async getUnits(refcat14, options = {}) {
434
+ return this.fincas.units(refcat14, options);
435
+ }
436
+ iterUnitPages(refcat14, options = {}) {
437
+ return this.fincas.pages(refcat14, options);
218
438
  }
219
439
  async resolve(text, options = {}) {
220
440
  return this.request("GET", "/api/resolve", { query: { q: text, hint: options.hint } });
221
441
  }
222
442
  async request(method, path, options = {}) {
443
+ const result = await this.conditionalRequest(method, path, options);
444
+ return result.data;
445
+ }
446
+ async conditionalRequest(method, path, options = {}) {
223
447
  const url = this.buildUrl(path, options.query);
224
448
  let attempt = 0;
225
449
  for (; ; ) {
@@ -241,6 +465,7 @@ var CatastroGPS = class {
241
465
  "User-Agent": `catastrogps-js/${VERSION}`
242
466
  };
243
467
  if (options.body !== void 0) headers["Content-Type"] = "application/json";
468
+ if (options.ifNoneMatch) headers["If-None-Match"] = options.ifNoneMatch;
244
469
  let response;
245
470
  try {
246
471
  response = await this.fetchImpl(url, {
@@ -257,15 +482,22 @@ var CatastroGPS = class {
257
482
  } finally {
258
483
  clearTimeout(timer);
259
484
  }
260
- this.captureQuota(response.headers);
485
+ const quota = this.captureQuota(response.headers);
486
+ const etag = response.headers.get("ETag") ?? void 0;
487
+ if (response.status === NOT_MODIFIED) return { etag: etag ?? options.ifNoneMatch, notModified: true, quota };
261
488
  if (!response.ok) {
262
489
  const body2 = await readJson(response);
263
490
  const retryAfter = Number(response.headers.get("Retry-After"));
264
- throw errorFromResponse(response.status, body2, Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0);
491
+ throw errorFromResponse(
492
+ response.status,
493
+ body2,
494
+ Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0,
495
+ readQuota(response.headers)
496
+ );
265
497
  }
266
- if (options.raw) return await response.arrayBuffer();
498
+ if (options.raw) return { data: await response.arrayBuffer(), etag, notModified: false, quota };
267
499
  const body = await readJson(response);
268
- return "data" in body ? body.data : body;
500
+ return { data: "data" in body ? body.data : body, etag, notModified: false, quota };
269
501
  }
270
502
  buildUrl(path, query) {
271
503
  const url = new URL(`${this.baseUrl}${path}`);
@@ -281,21 +513,56 @@ var CatastroGPS = class {
281
513
  return error instanceof CatastroGPSError && error.status !== void 0 && RETRYABLE_STATUS.has(error.status);
282
514
  }
283
515
  retryDelayMs(error, attempt) {
284
- if (error instanceof RateLimitError && error.retryAfterSeconds) return error.retryAfterSeconds * 1e3;
516
+ if ((error instanceof RateLimitError || error instanceof ServiceUnavailableError) && error.retryAfterSeconds) {
517
+ return Math.min(error.retryAfterSeconds, MAX_RETRY_AFTER_SECONDS) * 1e3;
518
+ }
285
519
  return BACKOFF_BASE_MS * 2 ** attempt;
286
520
  }
287
521
  captureQuota(headers) {
288
- const limit = headers.get("X-RateLimit-Limit");
289
- if (limit === null) return;
290
- const remaining = headers.get("X-RateLimit-Remaining");
291
- this.lastQuota = {
292
- plan: headers.get("X-Quota-Tier") ?? void 0,
293
- limit: Number(limit),
294
- remaining: remaining === null ? void 0 : Number(remaining),
295
- resetsAt: headers.get("X-RateLimit-Reset") ?? void 0
296
- };
522
+ const quota = readQuota(headers);
523
+ if (quota) this.lastQuota = quota;
524
+ return quota;
297
525
  }
298
526
  };
527
+ function readQuota(headers) {
528
+ const quota = {};
529
+ const plan = headers.get("X-Quota-Tier");
530
+ if (plan !== null) quota.plan = plan;
531
+ const limit = numberHeader(headers, "X-Quota-Limit");
532
+ if (limit !== void 0) quota.limit = limit;
533
+ const remaining = numberHeader(headers, "X-Quota-Remaining");
534
+ if (remaining !== void 0) quota.remaining = remaining;
535
+ const resetsAt = headers.get("X-Quota-Reset");
536
+ if (resetsAt !== null) quota.resetsAt = resetsAt;
537
+ if (headers.get("X-Quota-Overage")?.toLowerCase() === "on") {
538
+ quota.overage = {
539
+ enabled: true,
540
+ balance: numberHeader(headers, "X-Quota-Balance"),
541
+ unitPrice: numberHeader(headers, "X-Quota-Overage-Unit-Price"),
542
+ remainingUnits: numberHeader(headers, "X-Quota-Overage-Remaining")
543
+ };
544
+ }
545
+ if (headers.has("X-RateLimit-Limit") || headers.has("X-RateLimit-Remaining")) {
546
+ quota.rateLimit = {
547
+ limit: numberHeader(headers, "X-RateLimit-Limit"),
548
+ remaining: numberHeader(headers, "X-RateLimit-Remaining"),
549
+ resetSeconds: numberHeader(headers, "X-RateLimit-Reset")
550
+ };
551
+ }
552
+ if (headers.has("X-Quota-Finca-Cap")) {
553
+ quota.fincaCap = {
554
+ cap: numberHeader(headers, "X-Quota-Finca-Cap"),
555
+ used: numberHeader(headers, "X-Quota-Finca-Cap-Used")
556
+ };
557
+ }
558
+ return Object.keys(quota).length > 0 ? quota : void 0;
559
+ }
560
+ function numberHeader(headers, name) {
561
+ const raw = headers.get(name);
562
+ if (raw === null || raw.trim() === "") return void 0;
563
+ const value = Number(raw);
564
+ return Number.isFinite(value) ? value : void 0;
565
+ }
299
566
  async function readJson(response) {
300
567
  try {
301
568
  const parsed = await response.json();
@@ -311,44 +578,9 @@ function readEnv(name) {
311
578
  function sleep(ms) {
312
579
  return new Promise((resolve) => setTimeout(resolve, ms));
313
580
  }
314
-
315
- // src/types.ts
316
- var COUNTRY_CODES = [
317
- "ES",
318
- "PV",
319
- "NA",
320
- "PT",
321
- "FR",
322
- "IT",
323
- "DE",
324
- "AT",
325
- "CH",
326
- "LI",
327
- "BE",
328
- "NL",
329
- "LU",
330
- "PL",
331
- "CZ",
332
- "SK",
333
- "SI",
334
- "HR",
335
- "BG",
336
- "GR",
337
- "CY",
338
- "DK",
339
- "SE",
340
- "NO",
341
- "FI",
342
- "IS",
343
- "EE",
344
- "LV",
345
- "LT",
346
- "IE",
347
- "UK"
348
- ];
349
- var ENRICHMENT_COUNTRY_CODES = ["ES", "PV", "NA", "PT", "FR", "IT", "DE"];
350
581
  // Annotate the CommonJS export names for ESM import in node:
351
582
  0 && (module.exports = {
583
+ ADDRESS_COUNTRY_CODES,
352
584
  AmbiguousReferenceError,
353
585
  AuthenticationError,
354
586
  COUNTRY_CODES,
@@ -362,11 +594,14 @@ var ENRICHMENT_COUNTRY_CODES = ["ES", "PV", "NA", "PT", "FR", "IT", "DE"];
362
594
  ForbiddenError,
363
595
  NetworkError,
364
596
  NotFoundError,
597
+ PlaceNameError,
365
598
  QuotaExceededError,
366
599
  RateLimitError,
367
600
  ServerError,
368
601
  ServiceUnavailableError,
369
602
  TimeoutError,
603
+ UNITS_COUNTRY_CODES,
370
604
  VERSION,
371
- ValidationError
605
+ ValidationError,
606
+ readQuota
372
607
  });