@teryt24/api 0.0.0-stage → 0.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/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
+ }
@@ -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";