@teryt24/api 0.0.0-stage → 0.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 +110 -2
- package/dist/client.d.ts +289 -0
- package/dist/client.js +314 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/openapi.gen.d.ts +4296 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.js +10 -0
- package/package.json +37 -4
package/dist/client.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hand-written thin client for the teryt24.pl API (`@teryt24/api`).
|
|
3
|
+
*
|
|
4
|
+
* Three rules shape it:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Types come from the generated document.** Every method's params
|
|
7
|
+
* and response are indexed out of `openapi.gen.d.ts`, which
|
|
8
|
+
* scripts/build-sdk-types.ts generates from `GET /openapi.json` — the
|
|
9
|
+
* doc the routes' own zod schemas produce. A server-side field rename
|
|
10
|
+
* therefore becomes a typecheck failure in the SDK build (and in the
|
|
11
|
+
* consumer fixture), never a silent `undefined` in a customer's code.
|
|
12
|
+
* 2. **Errors are the published envelope.** Every /v1 error body carries
|
|
13
|
+
* `{error, message, code, details}` (docs/API-COMPATIBILITY.md §5);
|
|
14
|
+
* the client wraps it in a typed `Teryt24ApiError` whose `code` is the
|
|
15
|
+
* branch and whose `details` is the field-level array. Nothing here
|
|
16
|
+
* string-matches `error` or `message` — `code` is the contract.
|
|
17
|
+
* 3. **The documented 503s are retried once.** Warming up and the
|
|
18
|
+
* not-yet-ingested datasets answer 503 "try again shortly" on a
|
|
19
|
+
* freshly deployed instance; a first-deploy integration should survive
|
|
20
|
+
* that without an explicit retry loop, so the client retries a 503
|
|
21
|
+
* exactly once (configurable, and off when `retry503Once: false`).
|
|
22
|
+
*
|
|
23
|
+
* The client is typed for SERVER use: the API key travels in the
|
|
24
|
+
* `X-API-Key` header, and a browser would expose it to every visitor. The
|
|
25
|
+
* widget's documentation (https://teryt24.pl/docs/widget) describes the
|
|
26
|
+
* proxy pattern for browsers; this package does not pretend a key is safe
|
|
27
|
+
* client-side.
|
|
28
|
+
*/
|
|
29
|
+
import { SDK_VERSION } from "./version.js";
|
|
30
|
+
export { SDK_VERSION } from "./version.js";
|
|
31
|
+
/**
|
|
32
|
+
* A non-2xx answer from the API. `code` is the machine-readable branch
|
|
33
|
+
* (the table lives in docs/API-COMPATIBILITY.md §5), `details` the
|
|
34
|
+
* field-level array a 400 carries, and `body` the parsed body in full —
|
|
35
|
+
* new fields arrive without notice and stay reachable through it.
|
|
36
|
+
*/
|
|
37
|
+
export class Teryt24ApiError extends Error {
|
|
38
|
+
status;
|
|
39
|
+
code;
|
|
40
|
+
details;
|
|
41
|
+
body;
|
|
42
|
+
constructor(status, body) {
|
|
43
|
+
const envelope = (typeof body === "object" && body !== null ? body : {});
|
|
44
|
+
const message = typeof envelope.message === "string" && envelope.message.length > 0
|
|
45
|
+
? envelope.message
|
|
46
|
+
: `teryt24 API answered ${status}`;
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "Teryt24ApiError";
|
|
49
|
+
this.status = status;
|
|
50
|
+
this.code = typeof envelope.code === "string" ? envelope.code : null;
|
|
51
|
+
this.details = Array.isArray(envelope.details)
|
|
52
|
+
? envelope.details.filter((d) => typeof d === "object" &&
|
|
53
|
+
d !== null &&
|
|
54
|
+
typeof d.message === "string")
|
|
55
|
+
: [];
|
|
56
|
+
this.body = body;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The server's maximum `offset` (the paged endpoints' documented bound).
|
|
61
|
+
* `pages()` stops at it — see its JSDoc.
|
|
62
|
+
*/
|
|
63
|
+
const MAX_OFFSET = 10_000;
|
|
64
|
+
/**
|
|
65
|
+
* The thin client. Create one per process (or per credential) and reuse
|
|
66
|
+
* it: it holds no connection pool of its own, only configuration.
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* import { Teryt24Api } from "@teryt24/api";
|
|
70
|
+
*
|
|
71
|
+
* const teryt = new Teryt24Api({ apiKey: process.env.TERYT24_API_KEY });
|
|
72
|
+
* const { localities, meta } = await teryt.searchLocalities({ q: "wroc" });
|
|
73
|
+
* for await (const page of teryt.pages("localities", { q: "wroc" })) {
|
|
74
|
+
* // page.localities …
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export class Teryt24Api {
|
|
79
|
+
baseUrl;
|
|
80
|
+
apiKey;
|
|
81
|
+
fetchImpl;
|
|
82
|
+
timeoutMs;
|
|
83
|
+
retry503Once;
|
|
84
|
+
retryDelayMs;
|
|
85
|
+
extraHeaders;
|
|
86
|
+
constructor(options = {}) {
|
|
87
|
+
this.baseUrl = (options.baseUrl ?? "https://teryt24.pl").replace(/\/+$/, "");
|
|
88
|
+
this.apiKey = options.apiKey;
|
|
89
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
90
|
+
this.timeoutMs = options.timeoutMs ?? 10_000;
|
|
91
|
+
this.retry503Once = options.retry503Once ?? true;
|
|
92
|
+
this.retryDelayMs = options.retryDelayMs ?? 1_000;
|
|
93
|
+
this.extraHeaders = options.headers ?? {};
|
|
94
|
+
}
|
|
95
|
+
// --- /v1/localities --------------------------------------------------------
|
|
96
|
+
async searchLocalities(params) {
|
|
97
|
+
return this.get("/v1/localities", { query: params });
|
|
98
|
+
}
|
|
99
|
+
async locality(sym) {
|
|
100
|
+
return this.get("/v1/localities/{sym}", { path: { sym } });
|
|
101
|
+
}
|
|
102
|
+
// --- /v1/streets -------------------------------------------------------------
|
|
103
|
+
async searchStreets(params) {
|
|
104
|
+
return this.get("/v1/streets", { query: params });
|
|
105
|
+
}
|
|
106
|
+
async street(symUl) {
|
|
107
|
+
return this.get("/v1/streets/{symUl}", { path: { symUl } });
|
|
108
|
+
}
|
|
109
|
+
// --- /v1/postal-codes ----------------------------------------------------------
|
|
110
|
+
async searchPostalCodes(params) {
|
|
111
|
+
return this.get("/v1/postal-codes", { query: params });
|
|
112
|
+
}
|
|
113
|
+
async postalCode(code) {
|
|
114
|
+
return this.get("/v1/postal-codes/{code}", { path: { code } });
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* POST /v1/postal-codes/bulk — many codes, one request, one rate-limit
|
|
118
|
+
* slot. The response rows are the detail endpoint's rows, keyed by the
|
|
119
|
+
* caller's own `id` echo.
|
|
120
|
+
*/
|
|
121
|
+
async postalCodesBulk(body) {
|
|
122
|
+
return this.post("/v1/postal-codes/bulk", body);
|
|
123
|
+
}
|
|
124
|
+
// --- /v1/regions -------------------------------------------------------------------
|
|
125
|
+
async regions() {
|
|
126
|
+
return this.get("/v1/regions", {});
|
|
127
|
+
}
|
|
128
|
+
async regionCounties(code) {
|
|
129
|
+
return this.get("/v1/regions/{code}/counties", { path: { code } });
|
|
130
|
+
}
|
|
131
|
+
async regionMunicipalities(code, query = {}) {
|
|
132
|
+
return this.get("/v1/regions/{code}/municipalities", {
|
|
133
|
+
path: { code },
|
|
134
|
+
query,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async regionDemography(code) {
|
|
138
|
+
return this.get("/v1/regions/{code}/demography", { path: { code } });
|
|
139
|
+
}
|
|
140
|
+
async regionCensus(code) {
|
|
141
|
+
return this.get("/v1/regions/{code}/census", { path: { code } });
|
|
142
|
+
}
|
|
143
|
+
async areaCode(terc) {
|
|
144
|
+
return this.get("/v1/area-codes/{terc}", { path: { terc } });
|
|
145
|
+
}
|
|
146
|
+
// --- boundaries -----------------------------------------------------------------------
|
|
147
|
+
async boundaries(kind, query = {}) {
|
|
148
|
+
const path = kind === "regions" ? "/v1/boundaries/regions" : "/v1/boundaries/counties";
|
|
149
|
+
return this.getGeo(path, query);
|
|
150
|
+
}
|
|
151
|
+
// --- geocoding ---------------------------------------------------------------------------
|
|
152
|
+
async geocode(params) {
|
|
153
|
+
return this.get("/v1/geocode", { query: params });
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* POST /v1/geocode/batch — many addresses through the same pipeline
|
|
157
|
+
* GET /v1/geocode runs, one request. A malformed item answers as a
|
|
158
|
+
* `status: "error"` row; the HTTP status is 200 whenever the envelope
|
|
159
|
+
* itself parsed.
|
|
160
|
+
*/
|
|
161
|
+
async geocodeBatch(body) {
|
|
162
|
+
return this.post("/v1/geocode/batch", body);
|
|
163
|
+
}
|
|
164
|
+
async validate(params) {
|
|
165
|
+
return this.get("/v1/validate", { query: params });
|
|
166
|
+
}
|
|
167
|
+
async reverseGeocode(params) {
|
|
168
|
+
return this.get("/v1/reverse-geocode", { query: params });
|
|
169
|
+
}
|
|
170
|
+
async autosuggest(params) {
|
|
171
|
+
return this.get("/v1/autosuggest", { query: params });
|
|
172
|
+
}
|
|
173
|
+
// --- the feed and the meta surfaces ---------------------------------------------------------
|
|
174
|
+
async changes(params = {}) {
|
|
175
|
+
return this.get("/v1/changes", { query: params });
|
|
176
|
+
}
|
|
177
|
+
async datasets() {
|
|
178
|
+
return this.get("/v1/datasets", {});
|
|
179
|
+
}
|
|
180
|
+
async health() {
|
|
181
|
+
return this.get("/health", {});
|
|
182
|
+
}
|
|
183
|
+
async whoami() {
|
|
184
|
+
return this.get("/v1/whoami", {});
|
|
185
|
+
}
|
|
186
|
+
async usage(params = {}) {
|
|
187
|
+
return this.get("/v1/usage", { query: params });
|
|
188
|
+
}
|
|
189
|
+
async *pages(endpoint, params) {
|
|
190
|
+
let offset = typeof params.offset === "number" ? params.offset : 0;
|
|
191
|
+
for (;;) {
|
|
192
|
+
const page = (await this.getPage(endpoint, params));
|
|
193
|
+
yield page;
|
|
194
|
+
const meta = page.meta;
|
|
195
|
+
if (!meta)
|
|
196
|
+
return;
|
|
197
|
+
const next = meta.offset + meta.limit;
|
|
198
|
+
if (next >= meta.total)
|
|
199
|
+
return;
|
|
200
|
+
if (next > MAX_OFFSET)
|
|
201
|
+
return; // documented bound — see the JSDoc
|
|
202
|
+
params = { ...params, offset: next };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
getPage(endpoint, params) {
|
|
206
|
+
if (endpoint === "localities") {
|
|
207
|
+
return this.get("/v1/localities", { query: params });
|
|
208
|
+
}
|
|
209
|
+
if (endpoint === "postalCodes") {
|
|
210
|
+
return this.get("/v1/postal-codes", { query: params });
|
|
211
|
+
}
|
|
212
|
+
return this.get("/v1/changes", { query: params });
|
|
213
|
+
}
|
|
214
|
+
// --- the transport ---------------------------------------------------------------------------------
|
|
215
|
+
headers(extra) {
|
|
216
|
+
const headers = {
|
|
217
|
+
accept: "application/json",
|
|
218
|
+
"user-agent": `@teryt24/api/${SDK_VERSION}`,
|
|
219
|
+
...this.extraHeaders,
|
|
220
|
+
...extra,
|
|
221
|
+
};
|
|
222
|
+
if (this.apiKey !== undefined)
|
|
223
|
+
headers["x-api-key"] = this.apiKey;
|
|
224
|
+
return headers;
|
|
225
|
+
}
|
|
226
|
+
url(path, params) {
|
|
227
|
+
const search = new URLSearchParams();
|
|
228
|
+
for (const [key, value] of Object.entries(params ?? {})) {
|
|
229
|
+
if (value === undefined || value === null || value === "")
|
|
230
|
+
continue;
|
|
231
|
+
search.set(key, String(value));
|
|
232
|
+
}
|
|
233
|
+
const qs = search.toString();
|
|
234
|
+
return `${this.baseUrl}${path}${qs ? `?${qs}` : ""}`;
|
|
235
|
+
}
|
|
236
|
+
async request(url, init) {
|
|
237
|
+
const response = await this.fetchImpl(url, {
|
|
238
|
+
...init,
|
|
239
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
240
|
+
});
|
|
241
|
+
if (response.status === 503 && this.retry503Once) {
|
|
242
|
+
// The documented 503s (warming up, dataset not ingested, the
|
|
243
|
+
// handler budget) all mean "come back shortly". One retry is the
|
|
244
|
+
// client's own patience; a second failure is thrown as-is.
|
|
245
|
+
await new Promise((resolve) => setTimeout(resolve, this.retryDelayMs));
|
|
246
|
+
return this.fetchImpl(url, {
|
|
247
|
+
...init,
|
|
248
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return response;
|
|
252
|
+
}
|
|
253
|
+
async parse(response) {
|
|
254
|
+
if (response.ok) {
|
|
255
|
+
return (await response.json());
|
|
256
|
+
}
|
|
257
|
+
let body = null;
|
|
258
|
+
try {
|
|
259
|
+
body = await response.json();
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// A non-JSON error answer (an edge page, a proxy failure) still
|
|
263
|
+
// throws with the status; the envelope fields stay null.
|
|
264
|
+
}
|
|
265
|
+
throw new Teryt24ApiError(response.status, body);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The GET transport. The document's parameters object is split here:
|
|
269
|
+
* `path` fills the `{sym}`-shaped holes in the template, `query` rides
|
|
270
|
+
* the search string.
|
|
271
|
+
*/
|
|
272
|
+
async get(template, params) {
|
|
273
|
+
const split = params;
|
|
274
|
+
const named = split.path ?? {};
|
|
275
|
+
let concrete = template;
|
|
276
|
+
for (const [name, value] of Object.entries(named)) {
|
|
277
|
+
if (value === undefined || value === null) {
|
|
278
|
+
throw new TypeError(`@teryt24/api: missing path parameter "${name}" for ${template}`);
|
|
279
|
+
}
|
|
280
|
+
concrete = concrete.replace(`{${name}}`, encodeURIComponent(String(value)));
|
|
281
|
+
}
|
|
282
|
+
const url = this.url(concrete, split.query);
|
|
283
|
+
const response = await this.request(url, { method: "GET", headers: this.headers() });
|
|
284
|
+
return this.parse(response);
|
|
285
|
+
}
|
|
286
|
+
/** The boundaries endpoints answer `application/geo+json` (RFC 7946). */
|
|
287
|
+
async getGeo(path, params) {
|
|
288
|
+
const url = this.url(path, params);
|
|
289
|
+
const response = await this.request(url, { method: "GET", headers: this.headers() });
|
|
290
|
+
if (response.ok)
|
|
291
|
+
return (await response.json());
|
|
292
|
+
let body = null;
|
|
293
|
+
try {
|
|
294
|
+
body = await response.json();
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// as in parse()
|
|
298
|
+
}
|
|
299
|
+
throw new Teryt24ApiError(response.status, body);
|
|
300
|
+
}
|
|
301
|
+
async post(template, body) {
|
|
302
|
+
const url = this.url(template);
|
|
303
|
+
const response = await this.request(url, {
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
306
|
+
body: JSON.stringify(body),
|
|
307
|
+
});
|
|
308
|
+
return this.parse(response);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** A factory for the functional style; the class is the same thing. */
|
|
312
|
+
export function createTeryt24Api(options) {
|
|
313
|
+
return new Teryt24Api(options);
|
|
314
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@teryt24/api` — the official TypeScript client for the teryt24.pl API
|
|
3
|
+
* (Polish registry & geo data: GUS TERYT, GUGiK PRG, GeoNames postal).
|
|
4
|
+
*
|
|
5
|
+
* Types are generated from the served OpenAPI document; errors wrap the
|
|
6
|
+
* published `{error, message, code, details}` envelope; the documented
|
|
7
|
+
* 503s are retried once. Server use only — see the class docblock.
|
|
8
|
+
*/
|
|
9
|
+
export { SDK_VERSION } from "./version.js";
|
|
10
|
+
export { Teryt24Api, createTeryt24Api, Teryt24ApiError, } from "./client.js";
|
|
11
|
+
export type { Teryt24ClientOptions, ErrorDetail, ErrorBody, LocalitiesQuery, LocalitiesResponse, LocalityDetail, StreetsQuery, StreetsResponse, StreetDetail, PostalCodesQuery, PostalCodesResponse, PostalCodeDetail, PostalCodeRow, PostalCodesNearestRow, RegionsResponse, RegionCountiesResponse, RegionMunicipalitiesResponse, RegionDemography, RegionCensus, AreaCodeResponse, BoundaryFeatureCollection, GeocodeQuery, GeocodeResponse, GeocodeBatchRequest, GeocodeBatchResponse, ValidateQuery, ValidateResponse, ReverseGeocodeQuery, ReverseGeocodeResponse, AutosuggestQuery, AutosuggestResponse, ChangesQuery, ChangesResponse, DatasetsResponse, WhoamiResponse, UsageQuery, UsageResponse, HealthResponse, PostalBulkRequest, PostalBulkResponse, PagedEndpoint, } from "./client.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@teryt24/api` — the official TypeScript client for the teryt24.pl API
|
|
3
|
+
* (Polish registry & geo data: GUS TERYT, GUGiK PRG, GeoNames postal).
|
|
4
|
+
*
|
|
5
|
+
* Types are generated from the served OpenAPI document; errors wrap the
|
|
6
|
+
* published `{error, message, code, details}` envelope; the documented
|
|
7
|
+
* 503s are retried once. Server use only — see the class docblock.
|
|
8
|
+
*/
|
|
9
|
+
export { SDK_VERSION } from "./version.js";
|
|
10
|
+
export { Teryt24Api, createTeryt24Api, Teryt24ApiError, } from "./client.js";
|