lt-open-data-sdk 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
@@ -1,366 +1,371 @@
1
- # lt-open-data-sdk
2
-
3
- TypeScript SDK for the **Lithuanian Open Data platform** ([data.gov.lt](https://data.gov.lt)) powered by the Spinta engine.
4
-
5
- ## Features
6
-
7
- - 🔍 **QueryBuilder** - Fluent API for constructing DSQL queries
8
- - 🌐 **SpintaClient** - HTTP client with automatic pagination
9
- - 🛠️ **CLI Type Generator** - Generate TypeScript interfaces from live API
10
- - 🔐 **OAuth Support** - Client credentials authentication _(untested)_
11
-
12
- ## Installation
13
-
14
- ```bash
15
- npm install lt-open-data-sdk
16
- ```
17
-
18
- Requires Node.js ≥18 (uses native `fetch`).
19
-
20
- ## Quick Start
21
-
22
- ```typescript
23
- import { SpintaClient, QueryBuilder } from "lt-open-data-sdk";
24
-
25
- const client = new SpintaClient();
26
-
27
- // Fetch data with a query
28
- const query = new QueryBuilder()
29
- .select("_id", "pavadinimas")
30
- .filter((f) => f.field("sav_kodas").gt(10))
31
- .sort("pavadinimas")
32
- .limit(10);
33
-
34
- const data = await client.getAll(
35
- "datasets/gov/rc/ar/savivaldybe/Savivaldybe",
36
- query
37
- );
38
- console.log(data);
39
- ```
40
-
41
- ---
42
-
43
- ## QueryBuilder
44
-
45
- Build type-safe queries with a fluent API:
46
-
47
- ```typescript
48
- const query = new QueryBuilder<MyType>()
49
- .select("field1", "field2") // Select specific fields
50
- .filter((f) => f.field("x").eq(1)) // Add filters
51
- .sort("field1") // Sort ascending
52
- .sortDesc("field2") // Sort descending
53
- .limit(100); // Limit results
54
-
55
- const queryString = query.toQueryString();
56
- // Returns: ?select(field1,field2)&x=1&sort(field1,-field2)&limit(100)
57
- ```
58
-
59
- ### Filter Operators
60
-
61
- | Method | Query | Description |
62
- | ------------------ | ------------------------- | --------------------- |
63
- | `.eq(value)` | `field=value` | Equals |
64
- | `.ne(value)` | `field!=value` | Not equals |
65
- | `.lt(value)` | `field<value` | Less than |
66
- | `.le(value)` | `field<=value` | Less than or equal |
67
- | `.gt(value)` | `field>value` | Greater than |
68
- | `.ge(value)` | `field>=value` | Greater than or equal |
69
- | `.contains(str)` | `field.contains("str")` | Contains substring |
70
- | `.startswith(str)` | `field.startswith("str")` | Starts with |
71
-
72
- ### Combining Filters
73
-
74
- ```typescript
75
- // AND
76
- .filter(f => f.field('a').eq(1).and(f.field('b').eq(2)))
77
- // Output: a=1&b=2
78
-
79
- // OR
80
- .filter(f => f.field('a').eq(1).or(f.field('b').eq(2)))
81
- // Output: a=1|b=2
82
-
83
- // Complex (OR inside AND - auto-wrapped in parentheses)
84
- .filter(f => f.field('a').gt(10).and(
85
- f.field('b').eq(1).or(f.field('b').eq(2))
86
- ))
87
- // Output: a>10&(b=1|b=2)
88
- ```
89
-
90
- ---
91
-
92
- ## SpintaClient
93
-
94
- ### Basic Usage
95
-
96
- ```typescript
97
- const client = new SpintaClient();
98
- // Uses https://get.data.gov.lt by default
99
- ```
100
-
101
- ### Methods
102
-
103
- #### `getAll(model, query?)` - Fetch one page
104
-
105
- ```typescript
106
- const cities = await client.getAll("datasets/gov/example/City", query);
107
- // Returns: Array of objects (unwrapped from _data)
108
- ```
109
-
110
- > ⚠️ **Note**: Returns ONE page only. Use `stream()` for all records.
111
-
112
- #### `getAllRaw(model, query?)` - Fetch with metadata
113
-
114
- ```typescript
115
- const response = await client.getAllRaw("datasets/gov/example/City", query);
116
- // Returns: { _type, _data: [...], _page: { next } }
117
- ```
118
-
119
- #### `getOne(model, id)` - Fetch by UUID
120
-
121
- ```typescript
122
- const city = await client.getOne("datasets/gov/example/City", "uuid-here");
123
- ```
124
-
125
- #### `count(model, query?)` - Count records
126
-
127
- ```typescript
128
- const total = await client.count("datasets/gov/example/City");
129
- const filtered = await client.count(
130
- "datasets/gov/example/City",
131
- new QueryBuilder().filter((f) => f.field("population").gt(100000))
132
- );
133
- ```
134
-
135
- #### `stream(model, query?)` - Paginated iteration
136
-
137
- ```typescript
138
- for await (const city of client.stream("datasets/gov/example/City")) {
139
- console.log(city.pavadinimas);
140
- }
141
- ```
142
-
143
- #### `listNamespace(namespace)` - List namespace contents
144
-
145
- ```typescript
146
- const items = await client.listNamespace("datasets/gov/rc");
147
- // Returns: [{ _id: 'path', _type: 'ns' | 'model', title? }]
148
- ```
149
-
150
- #### `discoverModels(namespace)` - Find all models recursively
151
-
152
- ```typescript
153
- // Discover all available models in a namespace
154
- const models = await client.discoverModels("datasets/gov/rc/ar");
155
- console.log(`Found ${models.length} models`);
156
-
157
- for (const model of models) {
158
- console.log(`${model.path} - ${model.title}`);
159
- }
160
-
161
- // Then generate types for a specific model:
162
- // npx lt-gen datasets/gov/rc/ar/savivaldybe -o ./types/savivaldybe.d.ts
163
- ```
164
-
165
- Returns: `{ path, title?, namespace }[]`
166
-
167
- ---
168
-
169
- ## Type Safety & Autocomplete
170
-
171
- The SDK provides full TypeScript support. The workflow is:
172
-
173
- 1. **Generate types** for your dataset:
174
-
175
- ```bash
176
- npx lt-gen datasets/gov/rc/ar/savivaldybe -o ./types/savivaldybe.d.ts
177
- ```
178
-
179
- 2. **Import and use** in your code:
180
-
181
- ```typescript
182
- import { SpintaClient } from "lt-open-data-sdk";
183
- import type { GovRcArSavivaldybe_Savivaldybe } from "./types/savivaldybe";
184
-
185
- const client = new SpintaClient();
186
-
187
- // Pass the type to the method to get full autocomplete!
188
- const data = await client.getAll<GovRcArSavivaldybe_Savivaldybe>(
189
- "datasets/gov/rc/ar/savivaldybe/Savivaldybe"
190
- );
191
-
192
- // ✅ TypeScript knows these fields exist:
193
- console.log(data[0].pavadinimas); // string
194
- console.log(data[0].sav_kodas); // number
195
- ```
196
-
197
- ---
198
-
199
- ## Pagination
200
-
201
- The API uses cursor-based pagination with `_page.next` tokens.
202
-
203
- ### Automatic Pagination with `stream()`
204
-
205
- Use `stream()` to iterate through all records automatically:
206
-
207
- ```typescript
208
- const query = new QueryBuilder().limit(100); // 100 items per page
209
-
210
- for await (const item of client.stream("datasets/gov/example/City", query)) {
211
- console.log(item.pavadinimas);
212
- // Automatically fetches next page when current page is exhausted
213
- }
214
- ```
215
-
216
- **How it works:**
217
-
218
- 1. Fetches first page with your query
219
- 2. Yields items one by one
220
- 3. When page exhausted, uses `_page.next` token to fetch next page
221
- 4. Continues until no more pages
222
-
223
- ### Manual Pagination with `getAllRaw()`
224
-
225
- For more control, handle pagination yourself:
226
-
227
- ```typescript
228
- let pageToken: string | undefined;
229
-
230
- do {
231
- // Build query with page token
232
- let query = new QueryBuilder().limit(100);
233
-
234
- const response = await client.getAllRaw("datasets/gov/example/City", query);
235
-
236
- // Process this page
237
- for (const item of response._data) {
238
- console.log(item);
239
- }
240
-
241
- // Get next page token
242
- pageToken = response._page?.next;
243
-
244
- // Note: You need to add page(token) to next request manually
245
- // This is handled automatically by stream()
246
- } while (pageToken);
247
- ```
248
-
249
- > **Tip**: Use `stream()` for most cases. Use `getAllRaw()` when you need access to page metadata or custom page handling.
250
-
251
- ## CLI Type Generator
252
-
253
- Generate TypeScript interfaces from live API metadata:
254
-
255
- ```bash
256
- # Install globally or use npx
257
- npx lt-gen datasets/gov/rc/ar/savivaldybe
258
-
259
- # Save to file
260
- npx lt-gen datasets/gov/rc/ar/savivaldybe -o ./types/savivaldybe.d.ts
261
-
262
- # Custom API URL
263
- npx lt-gen datasets/gov/rc/ar/savivaldybe --base-url https://get-test.data.gov.lt
264
- ```
265
-
266
- ### Generated Output
267
-
268
- ```typescript
269
- export interface GovRcArSavivaldybe_Savivaldybe {
270
- _id: string;
271
- _type: string;
272
- _revision?: string;
273
- sav_kodas?: number;
274
- pavadinimas?: string;
275
- apskritis?: string | { _id: string }; // ref type
276
- sav_nuo?: string; // date
277
- }
278
-
279
- export interface ModelMap {
280
- "datasets/gov/rc/ar/savivaldybe/Savivaldybe": GovRcArSavivaldybe_Savivaldybe;
281
- }
282
- ```
283
-
284
- > **Note**: Types are inferred from data samples since schema endpoints require authentication.
285
-
286
- ---
287
-
288
- ## Authentication _(Untested)_
289
-
290
- For write operations or private data, provide OAuth credentials:
291
-
292
- ```typescript
293
- const client = new SpintaClient({
294
- clientId: "your-client-id",
295
- clientSecret: "your-client-secret",
296
- authUrl: "https://put.data.gov.lt", // optional, default
297
- scopes: ["spinta_getone", "spinta_getall"], // optional
298
- });
299
- ```
300
-
301
- The SDK handles:
302
-
303
- - OAuth client credentials flow
304
- - Automatic token caching
305
- - Token refresh before expiry (5-minute buffer)
306
-
307
- > ⚠️ **Note**: Authentication has been implemented but not tested against a live auth server.
308
-
309
- ---
310
-
311
- ## Error Handling
312
-
313
- ```typescript
314
- import {
315
- SpintaError,
316
- NotFoundError,
317
- AuthenticationError,
318
- ValidationError,
319
- } from "lt-open-data-sdk";
320
-
321
- try {
322
- const data = await client.getOne("datasets/example", "invalid-id");
323
- } catch (error) {
324
- if (error instanceof NotFoundError) {
325
- console.log("Not found:", error.message);
326
- } else if (error instanceof AuthenticationError) {
327
- console.log("Auth failed:", error.status);
328
- } else if (error instanceof ValidationError) {
329
- console.log("Bad request:", error.body);
330
- }
331
- }
332
- ```
333
-
334
- ---
335
-
336
- ## API Reference
337
-
338
- ### Exports
339
-
340
- ```typescript
341
- // Client
342
- export { SpintaClient } from "./client/SpintaClient";
343
- export {
344
- SpintaError,
345
- AuthenticationError,
346
- NotFoundError,
347
- ValidationError,
348
- } from "./client/errors";
349
-
350
- // Query Builder
351
- export { QueryBuilder } from "./builder/QueryBuilder";
352
- export { FilterBuilder } from "./builder/FilterBuilder";
353
-
354
- // Types
355
- export type {
356
- ClientConfig,
357
- SpintaResponse,
358
- SpintaObject,
359
- } from "./client/types";
360
- ```
361
-
362
- ---
363
-
364
- ## License
365
-
366
- MIT
1
+ # lt-open-data-sdk
2
+
3
+ A TypeScript SDK for accessing **Lithuania's Open Data Portal** ([data.gov.lt](https://data.gov.lt)).
4
+
5
+ ## What is this?
6
+
7
+ Lithuania publishes thousands of government datasets through its Open Data Portal, powered by the [Spinta](https://docs.data.gov.lt/projects/atviriduomenys/latest/api/) API engine. This SDK makes it easy to:
8
+
9
+ - **Query data** with a fluent, type-safe API instead of crafting raw URL parameters
10
+ - **Generate TypeScript types** from live datasets for full autocomplete support
11
+ - **Paginate automatically** through large datasets with async iterators
12
+ - **Track changes** for incremental data synchronization
13
+
14
+ ### Quick Links
15
+
16
+ - [📡 API Reference](#api) - Client methods and query builder
17
+ - [⌨️ CLI Reference](#cli) - Type generation commands
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install lt-open-data-sdk
25
+ ```
26
+
27
+ Requires Node.js ≥18.
28
+
29
+ ## Quick Example
30
+
31
+ ```typescript
32
+ import { SpintaClient, QueryBuilder } from "lt-open-data-sdk";
33
+
34
+ const client = new SpintaClient();
35
+
36
+ // Find municipalities with code greater than 30
37
+ const query = new QueryBuilder()
38
+ .filter((f) => f.field("sav_kodas").gt(30))
39
+ .sort("pavadinimas")
40
+ .limit(10);
41
+
42
+ const municipalities = await client.getAll(
43
+ "datasets/gov/rc/ar/savivaldybe/Savivaldybe",
44
+ query
45
+ );
46
+
47
+ console.log(municipalities);
48
+ ```
49
+
50
+ ---
51
+
52
+ ## API
53
+
54
+ The SDK provides a `SpintaClient` for making requests and a `QueryBuilder` for constructing queries.
55
+
56
+ ### Client Setup
57
+
58
+ ```typescript
59
+ import { SpintaClient } from "lt-open-data-sdk";
60
+
61
+ const client = new SpintaClient();
62
+ // Connects to https://get.data.gov.lt by default
63
+
64
+ // Or specify a custom base URL:
65
+ const client = new SpintaClient({
66
+ baseUrl: "https://get-test.data.gov.lt",
67
+ });
68
+ ```
69
+
70
+ ### Data Retrieval
71
+
72
+ #### `getAll(model, query?)` — Fetch records
73
+
74
+ Returns an array of records from a dataset. Use with `QueryBuilder` to filter, sort, and limit.
75
+
76
+ ````typescript
77
+ const localities = await client.getAll(
78
+ "datasets/gov/rc/ar/gyvenamojivietove/GyvenamojiVietove"
79
+ );
80
+ // Returns: [{ _id, _type, pavadinimas, tipas, ... }, ...]
81
+
82
+ > ⚠️ Returns one page only (default 100 items). Use `stream()` for all records.
83
+
84
+ #### `getOne(model, id)` — Fetch by ID
85
+
86
+ Returns a single record by its UUID.
87
+
88
+ ```typescript
89
+ const locality = await client.getOne(
90
+ "datasets/gov/rc/ar/gyvenamojivietove/GyvenamojiVietove",
91
+ "b19e801d-95d9-401f-8b00-b70b5f971f0e"
92
+ );
93
+ ````
94
+
95
+ #### `getAllRaw(model, query?)` — Fetch with metadata
96
+
97
+ Returns the full API response including pagination info.
98
+
99
+ ```typescript
100
+ const response = await client.getAllRaw("datasets/gov/rc/ar/miestas/Miestas");
101
+ // Returns: { _type, _data: [...], _page: { next: "token" } }
102
+ ```
103
+
104
+ #### `count(model, query?)` — Count records
105
+
106
+ Returns the total number of records matching the query.
107
+
108
+ ```typescript
109
+ const total = await client.count("datasets/gov/rc/ar/savivaldybe/Savivaldybe");
110
+
111
+ const filtered = await client.count(
112
+ "datasets/gov/rc/ar/savivaldybe/Savivaldybe",
113
+ new QueryBuilder().filter((f) => f.field("pavadinimas").contains("Vilni"))
114
+ );
115
+ ```
116
+
117
+ #### `stream(model, query?)` — Iterate all records
118
+
119
+ Async iterator that automatically handles pagination.
120
+
121
+ ```typescript
122
+ for await (const municipality of client.stream(
123
+ "datasets/gov/rc/ar/savivaldybe/Savivaldybe"
124
+ )) {
125
+ console.log(municipality.pavadinimas);
126
+ // Automatically fetches next pages
127
+ }
128
+ ```
129
+
130
+ ### Discovery
131
+
132
+ #### `listNamespace(namespace)` — Browse datasets
133
+
134
+ Lists namespaces and models within a path.
135
+
136
+ ```typescript
137
+ const items = await client.listNamespace("datasets/gov/rc");
138
+ // Returns: [{ _id: "datasets/gov/rc/ar", _type: "ns" }, ...]
139
+ ```
140
+
141
+ #### `discoverModels(namespace)` — Find all models
142
+
143
+ Recursively discovers all data models in a namespace.
144
+
145
+ ```typescript
146
+ const models = await client.discoverModels("datasets/gov/rc/ar");
147
+ console.log(`Found ${models.length} models`);
148
+ // Returns: [{ path, title, namespace }, ...]
149
+ ```
150
+
151
+ ### Changes API
152
+
153
+ Track data modifications for incremental sync.
154
+
155
+ #### `getLatestChange(model)` Get most recent change
156
+
157
+ ```typescript
158
+ const latest = await client.getLatestChange("datasets/gov/uzt/ldv/Vieta");
159
+ if (latest) {
160
+ console.log(`Last change: ${latest._op} at ${latest._created}`);
161
+ console.log(`Change ID: ${latest._cid}`);
162
+ }
163
+ // Returns: ChangeEntry | null
164
+ ```
165
+
166
+ #### `getChanges(model, sinceId?, limit?)` — Fetch changes
167
+
168
+ Returns changes since a given change ID.
169
+
170
+ ```typescript
171
+ const changes = await client.getChanges(
172
+ "datasets/gov/uzt/ldv/Vieta",
173
+ 0, // Start from beginning
174
+ 100 // Max 100 changes
175
+ );
176
+ // Returns: [{ _cid, _created, _op, _id, _data }, ...]
177
+ ```
178
+
179
+ #### `streamChanges(model, sinceId?, pageSize?)` Stream all changes
180
+
181
+ Async iterator for processing all changes with automatic pagination.
182
+
183
+ ```typescript
184
+ for await (const change of client.streamChanges(
185
+ "datasets/gov/uzt/ldv/Vieta",
186
+ lastKnownCid
187
+ )) {
188
+ console.log(`${change._op}: ${change._id}`);
189
+ }
190
+ ```
191
+
192
+ ---
193
+
194
+ ### QueryBuilder
195
+
196
+ Build queries with a fluent API.
197
+
198
+ ```typescript
199
+ import { QueryBuilder } from "lt-open-data-sdk";
200
+
201
+ const query = new QueryBuilder()
202
+ .select("_id", "pavadinimas", "gyventoju_skaicius")
203
+ .filter((f) => f.field("gyventoju_skaicius").gt(10000))
204
+ .sort("pavadinimas")
205
+ .limit(50);
206
+
207
+ const data = await client.getAll("datasets/gov/example/Model", query);
208
+ ```
209
+
210
+ #### Filter Operators
211
+
212
+ | Method | Query | Description |
213
+ | ------------------ | ------------------------- | --------------------- |
214
+ | `.eq(value)` | `field=value` | Equals |
215
+ | `.ne(value)` | `field!=value` | Not equals |
216
+ | `.lt(value)` | `field<value` | Less than |
217
+ | `.le(value)` | `field<=value` | Less than or equal |
218
+ | `.gt(value)` | `field>value` | Greater than |
219
+ | `.ge(value)` | `field>=value` | Greater than or equal |
220
+ | `.contains(str)` | `field.contains("str")` | Contains substring |
221
+ | `.startswith(str)` | `field.startswith("str")` | Starts with |
222
+ | `.endswith(str)` | `field.endswith("str")` | Ends with ⚠️ |
223
+ | `.in([...])` | `field.in(a,b,c)` | Value in list ⚠️ |
224
+ | `.notin([...])` | `field.notin(a,b,c)` | Value not in list ⚠️ |
225
+
226
+ > ⚠️ `endswith`, `in`, `notin` are in the Spinta spec but not yet supported by the live API.
227
+
228
+ #### Combining Filters
229
+
230
+ ```typescript
231
+ // AND - both conditions must match
232
+ .filter(f => f.field('a').gt(10).and(f.field('b').lt(100)))
233
+ // Output: a>10&b<100
234
+
235
+ // OR - either condition matches
236
+ .filter(f => f.field('status').eq('active').or(f.field('status').eq('pending')))
237
+ // Output: status="active"|status="pending"
238
+
239
+ // Complex - parentheses added automatically
240
+ .filter(f => f.field('a').gt(10).and(
241
+ f.field('b').eq(1).or(f.field('b').eq(2))
242
+ ))
243
+ // Output: a>10&(b=1|b=2)
244
+ ```
245
+
246
+ #### Sorting
247
+
248
+ ```typescript
249
+ new QueryBuilder()
250
+ .sort("name") // Ascending
251
+ .sortDesc("created_at"); // Descending
252
+ // Output: ?sort(name,-created_at)
253
+ ```
254
+
255
+ ---
256
+
257
+ ## CLI
258
+
259
+ Generate TypeScript interfaces from live API data.
260
+
261
+ ### Basic Usage
262
+
263
+ ```bash
264
+ # Generate types for a dataset (prints to stdout)
265
+ npx lt-gen datasets/gov/rc/ar/savivaldybe
266
+
267
+ # Save to a file
268
+ npx lt-gen datasets/gov/rc/ar/savivaldybe -o ./types/savivaldybe.d.ts
269
+
270
+ # Use a different API endpoint
271
+ npx lt-gen datasets/gov/rc/ar/savivaldybe --base-url https://get-test.data.gov.lt
272
+ ```
273
+
274
+ ### Options
275
+
276
+ | Option | Description |
277
+ | --------------------- | -------------------- |
278
+ | `-o, --output <file>` | Write output to file |
279
+ | `--base-url <url>` | Custom API base URL |
280
+ | `-h, --help` | Show help |
281
+
282
+ ### Generated Output
283
+
284
+ ```typescript
285
+ // Generated from datasets/gov/rc/ar/savivaldybe/Savivaldybe
286
+
287
+ export interface GovRcArSavivaldybe_Savivaldybe {
288
+ _id: string;
289
+ _type: string;
290
+ _revision?: string;
291
+ sav_kodas?: number;
292
+ pavadinimas?: string;
293
+ apskritis?: string | { _id: string };
294
+ sav_nuo?: string;
295
+ }
296
+
297
+ export interface ModelMap {
298
+ "datasets/gov/rc/ar/savivaldybe/Savivaldybe": GovRcArSavivaldybe_Savivaldybe;
299
+ }
300
+ ```
301
+
302
+ ### Using Generated Types
303
+
304
+ ```typescript
305
+ import { SpintaClient } from "lt-open-data-sdk";
306
+ import type { GovRcArSavivaldybe_Savivaldybe } from "./types/savivaldybe";
307
+
308
+ const client = new SpintaClient();
309
+
310
+ // Full autocomplete on fields!
311
+ const data = await client.getAll<GovRcArSavivaldybe_Savivaldybe>(
312
+ "datasets/gov/rc/ar/savivaldybe/Savivaldybe"
313
+ );
314
+
315
+ console.log(data[0].pavadinimas); // TypeScript knows this is string
316
+ console.log(data[0].sav_kodas); // TypeScript knows this is number
317
+ ```
318
+
319
+ ---
320
+
321
+ ## Error Handling
322
+
323
+ ```typescript
324
+ import {
325
+ SpintaError,
326
+ NotFoundError,
327
+ AuthenticationError,
328
+ ValidationError,
329
+ } from "lt-open-data-sdk";
330
+
331
+ try {
332
+ const data = await client.getOne("datasets/example", "invalid-id");
333
+ } catch (error) {
334
+ if (error instanceof NotFoundError) {
335
+ console.log("Record not found");
336
+ } else if (error instanceof ValidationError) {
337
+ console.log("Invalid query:", error.message);
338
+ }
339
+ }
340
+ ```
341
+
342
+ ---
343
+
344
+ ## Authentication
345
+
346
+ For write operations or private datasets, provide OAuth credentials:
347
+
348
+ ```typescript
349
+ const client = new SpintaClient({
350
+ clientId: "your-client-id",
351
+ clientSecret: "your-client-secret",
352
+ });
353
+ ```
354
+
355
+ The SDK handles token caching and automatic refresh.
356
+
357
+ > ⚠️ Authentication is implemented but untested against the live auth server.
358
+
359
+ ---
360
+
361
+ ## Known Limitations
362
+
363
+ - **Boolean filtering** may not work on some datasets due to inconsistent data formats in the source
364
+ - **`in()`, `notin()`, `endswith()`** operators are implemented but not yet supported by the live API
365
+ - **Type inference** is based on data sampling, not schema (schema endpoints require auth)
366
+
367
+ ---
368
+
369
+ ## License
370
+
371
+ MIT