@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 CHANGED
@@ -1,3 +1,111 @@
1
- # Temporary Holding Version
1
+ # @teryt24/api
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The official TypeScript client for the [teryt24.pl API](https://teryt24.pl/docs) —
4
+ Polish registry & geo data: localities and cities (GUS TERYT SIMC), streets
5
+ (ULIC), postal codes (PNA), address validation and geocoding (GUGiK PRG
6
+ address points).
7
+
8
+ - Types are **generated from the served OpenAPI document** — a server-side
9
+ field rename breaks the build, not your code at runtime.
10
+ - Errors wrap the published `{error, message, code, details}` envelope
11
+ ([the error contract](https://teryt24.pl/docs)): `Teryt24ApiError.code`
12
+ is the branch, `details` the field-level array.
13
+ - The documented **503s are retried once** ("warming up / dataset not
14
+ ingested"), so a first-deploy integration does not crash on them.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @teryt24/api
20
+ ```
21
+
22
+ ## Use
23
+
24
+ ```js
25
+ import { Teryt24Api } from "@teryt24/api";
26
+
27
+ const teryt = new Teryt24Api({
28
+ apiKey: process.env.TERYT24_API_KEY, // optional — keyless calls share a 60 req/min/IP bucket
29
+ });
30
+
31
+ // Locality autocomplete (GUS SIMC, ≈54k entries)
32
+ const { localities } = await teryt.searchLocalities({ q: "wroc", limit: 5 });
33
+
34
+ // One locality's full record (postal codes, street count, census)
35
+ const detail = await teryt.locality("0986745");
36
+
37
+ // Street search — within one locality, or country-wide
38
+ const { streets } = await teryt.searchStreets({ city: "0986745", q: "legnicka" });
39
+
40
+ // Postal codes — prefix, per-locality, or nearest (PRG centroids)
41
+ const { postalCodes } = await teryt.searchPostalCodes({ q: "59-7", limit: 10 });
42
+
43
+ // Address validation — the verdict, per-field match kinds, issues
44
+ const verdict = await teryt.validate({
45
+ address: "Wrocław, ul. Eugeniusza Horbaczewskiego 35",
46
+ });
47
+
48
+ // Geocoding to the house number (GUGiK PRG points)
49
+ const point = await teryt.geocode({ sym: "0986745", streetName: "horbaczewskiego", number: "35" });
50
+ ```
51
+
52
+ Every method is typed from the API's own OpenAPI document — editor
53
+ completion shows the real query parameters and response fields.
54
+
55
+ ## Paging
56
+
57
+ The paged endpoints (`localities`, `postalCodes`, `changes`) carry
58
+ `meta: { total, offset, limit }`. The `pages()` async iterator walks them:
59
+
60
+ ```js
61
+ for await (const page of teryt.pages("localities", { province: "02" })) {
62
+ for (const locality of page.localities) console.log(locality.name);
63
+ }
64
+ ```
65
+
66
+ **Where it breaks:** the API's documented maximum `offset` is 10 000. When
67
+ the next page would sit beyond it, the iterator ends after the page it
68
+ just yielded — narrow the query instead (filters, a longer prefix, a date
69
+ window).
70
+
71
+ ## Errors
72
+
73
+ ```js
74
+ import { Teryt24ApiError } from "@teryt24/api";
75
+
76
+ try {
77
+ await teryt.locality("0000000");
78
+ } catch (error) {
79
+ if (error instanceof Teryt24ApiError) {
80
+ error.status; // 404
81
+ error.code; // "locality_not_found" — branch on this, never on message
82
+ error.details; // field-level array on 400s
83
+ error.message; // a human sentence (wording may change without notice)
84
+ }
85
+ }
86
+ ```
87
+
88
+ The full table of codes lives in [the compatibility policy](https://teryt24.pl/docs)
89
+ (`docs/API-COMPATIBILITY.md` §5 in the service's repository). `5xx` mean
90
+ "retry later" (503s are retried once for you); `429` carries `Retry-After`.
91
+
92
+ ## Keys and browsers
93
+
94
+ The API key travels in the `X-API-Key` header. This client is typed for
95
+ **server use** — a browser would expose the key to every visitor. For
96
+ browser forms use the keyless API directly, or proxy through your own
97
+ backend: see [the address widget's API-key proxy guidance](https://teryt24.pl/docs/widget).
98
+
99
+ ## Attribution
100
+
101
+ When you display results in an application, website, map or report,
102
+ attribute the data — `Data: GUS TERYT, PRG address points (GUGiK),
103
+ GeoNames postal codes — via teryt24.pl` — per the
104
+ [usage policy](https://teryt24.pl/usage-policy). Server-side-only use is
105
+ exempt.
106
+
107
+ ## The MCP server
108
+
109
+ An agent that should call this API through tool calls can use
110
+ [`@teryt24/mcp`](https://www.npmjs.com/package/@teryt24/mcp) — a stdio MCP
111
+ server built on this package (12 tools, responses verbatim).
@@ -0,0 +1,289 @@
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 type { paths } from "./openapi.gen.js";
30
+ export { SDK_VERSION } from "./version.js";
31
+ /** One field-level validation detail, as the error envelope carries it. */
32
+ export interface ErrorDetail {
33
+ path: string;
34
+ message: string;
35
+ }
36
+ /** The body keys a /v1 error answer carries (docs/API-COMPATIBILITY.md §5). */
37
+ export interface ErrorBody {
38
+ error?: string;
39
+ message?: string;
40
+ code?: string;
41
+ details?: ErrorDetail[];
42
+ [key: string]: unknown;
43
+ }
44
+ /**
45
+ * A non-2xx answer from the API. `code` is the machine-readable branch
46
+ * (the table lives in docs/API-COMPATIBILITY.md §5), `details` the
47
+ * field-level array a 400 carries, and `body` the parsed body in full —
48
+ * new fields arrive without notice and stay reachable through it.
49
+ */
50
+ export declare class Teryt24ApiError extends Error {
51
+ readonly status: number;
52
+ readonly code: string | null;
53
+ readonly details: readonly ErrorDetail[];
54
+ readonly body: unknown;
55
+ constructor(status: number, body: unknown);
56
+ }
57
+ export interface Teryt24ClientOptions {
58
+ /**
59
+ * The API key sent as `X-API-Key` (raises the rate limit and is what
60
+ * the metering applies to). Optional: the API serves keyless callers
61
+ * from a shared 60 req/min/IP bucket.
62
+ */
63
+ apiKey?: string;
64
+ /** The service base URL. Default: `https://teryt24.pl`. */
65
+ baseUrl?: string;
66
+ /** The fetch implementation. Default: the global one (Node ≥ 18). */
67
+ fetch?: typeof fetch;
68
+ /** Per-attempt timeout in milliseconds. Default: 10 000. */
69
+ timeoutMs?: number;
70
+ /**
71
+ * Retry a 503 exactly once (the documented "warming up / dataset not
72
+ * ingested" contract). Default: true.
73
+ */
74
+ retry503Once?: boolean;
75
+ /** The wait before that one retry, in milliseconds. Default: 1 000. */
76
+ retryDelayMs?: number;
77
+ /** Extra headers (a meaningful `User-Agent` naming your application is encouraged by the usage policy). */
78
+ headers?: Record<string, string>;
79
+ }
80
+ /**
81
+ * The parameters object of one GET endpoint, as the generated document
82
+ * declares it: `{ query?, path?, header?, cookie? }` with `never` for the
83
+ * kinds the endpoint does not take. A method passes the members it needs
84
+ * (`{ query }` or `{ path: { sym } }`); the private transport splits them.
85
+ */
86
+ type GetParametersOf<Path extends keyof paths & string> = paths[Path] extends {
87
+ get: {
88
+ parameters: infer P;
89
+ };
90
+ } ? P : never;
91
+ /** The 200 JSON body of one GET endpoint, from the generated document. */
92
+ type BodyOf<Path extends keyof paths & string> = paths[Path] extends {
93
+ get: {
94
+ responses: {
95
+ 200: {
96
+ content: {
97
+ "application/json": infer B;
98
+ };
99
+ };
100
+ };
101
+ };
102
+ } ? B : never;
103
+ /** The 200 JSON body of one POST endpoint (the batch/bulk endpoints). */
104
+ type PostBodyOf<Path extends keyof paths & string> = paths[Path] extends {
105
+ post: {
106
+ responses: {
107
+ 200: {
108
+ content: {
109
+ "application/json": infer B;
110
+ };
111
+ };
112
+ };
113
+ };
114
+ } ? B : never;
115
+ /** The JSON request body of one POST endpoint, from the document. */
116
+ type PostRequestOf<Path extends keyof paths & string> = paths[Path] extends {
117
+ post: {
118
+ requestBody: {
119
+ content: {
120
+ "application/json": infer B;
121
+ };
122
+ };
123
+ };
124
+ } ? B : never;
125
+ /** The query type of one GET endpoint (the method signatures' ergonomics). */
126
+ type QueryOf<Path extends keyof paths & string> = GetParametersOf<Path> extends {
127
+ query?: infer Q;
128
+ } ? NonNullable<Q> : never;
129
+ export type LocalitiesQuery = QueryOf<"/v1/localities">;
130
+ export type LocalitiesResponse = BodyOf<"/v1/localities">;
131
+ export type LocalityDetail = BodyOf<"/v1/localities/{sym}">;
132
+ export type StreetsQuery = QueryOf<"/v1/streets">;
133
+ export type StreetsResponse = BodyOf<"/v1/streets">;
134
+ export type StreetDetail = BodyOf<"/v1/streets/{symUl}">;
135
+ export type PostalCodesQuery = QueryOf<"/v1/postal-codes">;
136
+ export type PostalCodesResponse = BodyOf<"/v1/postal-codes">;
137
+ export type PostalCodeDetail = BodyOf<"/v1/postal-codes/{code}">;
138
+ /** One row of either postal-codes list mode (the document's response union). */
139
+ export type PostalCodeRow = PostalCodesResponse extends {
140
+ postalCodes: (infer R)[];
141
+ } ? R : never;
142
+ /** The nearest-mode row, narrowed by its `distanceKm` field. */
143
+ export type PostalCodesNearestRow = Extract<PostalCodeRow, {
144
+ distanceKm: number;
145
+ }>;
146
+ export type RegionsResponse = BodyOf<"/v1/regions">;
147
+ export type RegionCountiesResponse = BodyOf<"/v1/regions/{code}/counties">;
148
+ export type RegionMunicipalitiesResponse = BodyOf<"/v1/regions/{code}/municipalities">;
149
+ export type RegionDemography = BodyOf<"/v1/regions/{code}/demography">;
150
+ export type RegionCensus = BodyOf<"/v1/regions/{code}/census">;
151
+ export type AreaCodeResponse = BodyOf<"/v1/area-codes/{terc}">;
152
+ export type BoundaryFeatureCollection = paths["/v1/boundaries/regions"] extends {
153
+ get: {
154
+ responses: {
155
+ 200: {
156
+ content: {
157
+ "application/geo+json": infer B;
158
+ };
159
+ };
160
+ };
161
+ };
162
+ } ? B : never;
163
+ export type GeocodeQuery = QueryOf<"/v1/geocode">;
164
+ export type GeocodeResponse = BodyOf<"/v1/geocode">;
165
+ export type GeocodeBatchRequest = PostRequestOf<"/v1/geocode/batch">;
166
+ export type GeocodeBatchResponse = PostBodyOf<"/v1/geocode/batch">;
167
+ export type ValidateQuery = QueryOf<"/v1/validate">;
168
+ export type ValidateResponse = BodyOf<"/v1/validate">;
169
+ export type ReverseGeocodeQuery = QueryOf<"/v1/reverse-geocode">;
170
+ export type ReverseGeocodeResponse = BodyOf<"/v1/reverse-geocode">;
171
+ export type AutosuggestQuery = QueryOf<"/v1/autosuggest">;
172
+ export type AutosuggestResponse = BodyOf<"/v1/autosuggest">;
173
+ export type ChangesQuery = QueryOf<"/v1/changes">;
174
+ export type ChangesResponse = BodyOf<"/v1/changes">;
175
+ export type DatasetsResponse = BodyOf<"/v1/datasets">;
176
+ export type WhoamiResponse = BodyOf<"/v1/whoami">;
177
+ export type UsageQuery = QueryOf<"/v1/usage">;
178
+ export type UsageResponse = BodyOf<"/v1/usage">;
179
+ export type PostalBulkRequest = PostRequestOf<"/v1/postal-codes/bulk">;
180
+ export type PostalBulkResponse = PostBodyOf<"/v1/postal-codes/bulk">;
181
+ /**
182
+ * The deep `/health` view. The document types it as an open object (the
183
+ * advisory shape moves with the datasets an instance serves); the stable
184
+ * fields it carries are documented in the service's README and at
185
+ * https://teryt24.pl/docs.
186
+ */
187
+ export type HealthResponse = BodyOf<"/health">;
188
+ /** The paged endpoints the `pages()` iterator walks. */
189
+ export type PagedEndpoint = "localities" | "postalCodes" | "changes";
190
+ /**
191
+ * The thin client. Create one per process (or per credential) and reuse
192
+ * it: it holds no connection pool of its own, only configuration.
193
+ *
194
+ * ```ts
195
+ * import { Teryt24Api } from "@teryt24/api";
196
+ *
197
+ * const teryt = new Teryt24Api({ apiKey: process.env.TERYT24_API_KEY });
198
+ * const { localities, meta } = await teryt.searchLocalities({ q: "wroc" });
199
+ * for await (const page of teryt.pages("localities", { q: "wroc" })) {
200
+ * // page.localities …
201
+ * }
202
+ * ```
203
+ */
204
+ export declare class Teryt24Api {
205
+ private readonly baseUrl;
206
+ private readonly apiKey;
207
+ private readonly fetchImpl;
208
+ private readonly timeoutMs;
209
+ private readonly retry503Once;
210
+ private readonly retryDelayMs;
211
+ private readonly extraHeaders;
212
+ constructor(options?: Teryt24ClientOptions);
213
+ searchLocalities(params: LocalitiesQuery): Promise<LocalitiesResponse>;
214
+ locality(sym: string): Promise<LocalityDetail>;
215
+ searchStreets(params: StreetsQuery): Promise<StreetsResponse>;
216
+ street(symUl: string): Promise<StreetDetail>;
217
+ searchPostalCodes(params: PostalCodesQuery): Promise<PostalCodesResponse>;
218
+ postalCode(code: string): Promise<PostalCodeDetail>;
219
+ /**
220
+ * POST /v1/postal-codes/bulk — many codes, one request, one rate-limit
221
+ * slot. The response rows are the detail endpoint's rows, keyed by the
222
+ * caller's own `id` echo.
223
+ */
224
+ postalCodesBulk(body: PostalBulkRequest): Promise<PostalBulkResponse>;
225
+ regions(): Promise<RegionsResponse>;
226
+ regionCounties(code: string): Promise<RegionCountiesResponse>;
227
+ regionMunicipalities(code: string, query?: {
228
+ county?: string;
229
+ }): Promise<RegionMunicipalitiesResponse>;
230
+ regionDemography(code: string): Promise<RegionDemography>;
231
+ regionCensus(code: string): Promise<RegionCensus>;
232
+ areaCode(terc: string): Promise<AreaCodeResponse>;
233
+ boundaries(kind: "regions" | "counties", query?: {
234
+ province?: string;
235
+ }): Promise<BoundaryFeatureCollection>;
236
+ geocode(params: GeocodeQuery): Promise<GeocodeResponse>;
237
+ /**
238
+ * POST /v1/geocode/batch — many addresses through the same pipeline
239
+ * GET /v1/geocode runs, one request. A malformed item answers as a
240
+ * `status: "error"` row; the HTTP status is 200 whenever the envelope
241
+ * itself parsed.
242
+ */
243
+ geocodeBatch(body: GeocodeBatchRequest): Promise<GeocodeBatchResponse>;
244
+ validate(params: ValidateQuery): Promise<ValidateResponse>;
245
+ reverseGeocode(params: ReverseGeocodeQuery): Promise<ReverseGeocodeResponse>;
246
+ autosuggest(params: AutosuggestQuery): Promise<AutosuggestResponse>;
247
+ changes(params?: ChangesQuery): Promise<ChangesResponse>;
248
+ datasets(): Promise<DatasetsResponse>;
249
+ health(): Promise<HealthResponse>;
250
+ whoami(): Promise<WhoamiResponse>;
251
+ usage(params?: UsageQuery): Promise<UsageResponse>;
252
+ /**
253
+ * Walks a paged endpoint page by page, following `meta.total` /
254
+ * `meta.offset` / `meta.limit` until the selection is exhausted.
255
+ *
256
+ * **Where it breaks:** the API's documented maximum `offset` is 10 000
257
+ * (a deliberate depth bound — see /docs). When the NEXT page would sit
258
+ * beyond it, the iterator ENDS after the page it just yielded — it does
259
+ * not throw, and it does not silently continue from the other side. A
260
+ * selection deeper than 10 000 rows must be narrowed (province /
261
+ * county / municipality / population filters on localities; a longer
262
+ * prefix on postal codes; a date window on changes) — the requests this
263
+ * iterator would otherwise take are 400s, not data.
264
+ *
265
+ * Yields the same body the endpoint's own method returns (`rows` key +
266
+ * `meta`), so a consumer can switch between paging styles freely.
267
+ */
268
+ pages(endpoint: "localities", params: LocalitiesQuery): AsyncGenerator<LocalitiesResponse, void, undefined>;
269
+ pages(endpoint: "postalCodes", params: PostalCodesQuery): AsyncGenerator<PostalCodesResponse, void, undefined>;
270
+ pages(endpoint: "changes", params: ChangesQuery): AsyncGenerator<ChangesResponse, void, undefined>;
271
+ private getPage;
272
+ private headers;
273
+ private url;
274
+ private request;
275
+ private parse;
276
+ /**
277
+ * The GET transport. The document's parameters object is split here:
278
+ * `path` fills the `{sym}`-shaped holes in the template, `query` rides
279
+ * the search string.
280
+ */
281
+ private get;
282
+ /** The boundaries endpoints answer `application/geo+json` (RFC 7946). */
283
+ private getGeo;
284
+ private post;
285
+ }
286
+ /** A factory for the functional style; the class is the same thing. */
287
+ export declare function createTeryt24Api(options?: Teryt24ClientOptions): Teryt24Api;
288
+ /** Exposed for typed consumers that branch on a path's parameters object. */
289
+ export type { GetParametersOf, QueryOf, BodyOf, PostBodyOf, PostRequestOf };