@scarif/scarif-js 1.0.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 ADDED
@@ -0,0 +1,451 @@
1
+ # Stoorplek Data API SDK
2
+
3
+ Simple, type-safe SDK for accessing data from your API service with powerful chainable querying capabilities. Built with Supabase-style select syntax for intuitive querying.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install stoorplek-data-api
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### Option 1: True One-Liner (Environment Variable)
14
+
15
+ Set your API key in `.env`:
16
+ ```env
17
+ MY_API_KEY=sk_live_your_api_key_here
18
+ ```
19
+
20
+ Then use the one-liner:
21
+ ```javascript
22
+ import { stoorplek } from 'stoorplek-data-api';
23
+
24
+ // That's it! One import, one call
25
+ const { data, error } = await stoorplek.from('countries').select('*');
26
+ console.log(data);
27
+ ```
28
+
29
+ ### Option 2: Configure Once, Use Everywhere
30
+
31
+ ```javascript
32
+ import { stoorplek } from 'stoorplek-data-api';
33
+
34
+ // Configure once at app startup
35
+ stoorplek.configure({
36
+ apiKey: 'sk_live_your_api_key_here'
37
+ });
38
+
39
+ // Then use anywhere in your app
40
+ const { data: countries } = await stoorplek.from('countries').select('*');
41
+ const { data: cities } = await stoorplek.from('cities').select('*');
42
+ ```
43
+
44
+ ### Option 3: Traditional Client (More Control)
45
+
46
+ ```javascript
47
+ import { createClient } from 'stoorplek-data-api';
48
+
49
+ const client = createClient({
50
+ apiKey: 'sk_live_your_api_key_here',
51
+ baseUrl: 'https://api.yourservice.com', // optional
52
+ timeout: 30000 // optional, defaults to 30 seconds
53
+ });
54
+
55
+ const { data, error } = await client.from('countries').select('*');
56
+ ```
57
+
58
+ ## Features
59
+
60
+ ✨ **Type-Safe** - Full TypeScript support with automatic type inference
61
+ 🔍 **Chainable API** - Supabase-like query builder with fluent interface
62
+ 📦 **Zero Config** - Works with environment variables out of the box
63
+ 🎯 **One-Liner Ready** - Import and use in a single line
64
+ ⚡ **Lightweight** - Minimal dependencies, maximum performance
65
+ 🔗 **Relations** - Easy eager loading of related data
66
+
67
+ ## Querying & Filtering
68
+
69
+ ### Basic Queries
70
+
71
+ ```javascript
72
+ import { stoorplek } from 'stoorplek-data-api';
73
+
74
+ // Get all records (select is required)
75
+ const { data: all } = await stoorplek.from('countries').select('*');
76
+
77
+ // Select specific columns
78
+ const { data: names } = await stoorplek.from('countries')
79
+ .select('name, iso');
80
+ ```
81
+
82
+ ### Relations (Eager Loading)
83
+
84
+ Supabase-style nested relations in the select string:
85
+
86
+ ```javascript
87
+ // Load relations (all columns)
88
+ const { data: withCities } = await stoorplek.from('countries')
89
+ .select('*, cities(*)');
90
+
91
+ // Load relations with specific columns
92
+ const { data: custom } = await stoorplek.from('countries')
93
+ .select('name, iso, cities(id, name, population)');
94
+
95
+ // Multiple relations
96
+ const { data: multiple } = await stoorplek.from('countries')
97
+ .select('*, cities(*), languages(*)');
98
+
99
+ // Multiple relations with column selection
100
+ const { data: complex } = await stoorplek.from('countries')
101
+ .select('id, name, cities(id, name), languages(*)');
102
+ ```
103
+
104
+ ### Filtering
105
+
106
+ ```javascript
107
+ // Equal to
108
+ const { data: usa } = await stoorplek.from('countries')
109
+ .select('*')
110
+ .eq('iso', 'US');
111
+
112
+ // Not equal to
113
+ const { data: notUSA } = await stoorplek.from('countries')
114
+ .select('*')
115
+ .neq('iso', 'US');
116
+
117
+ // Greater than / Less than
118
+ const { data: highIds } = await stoorplek.from('countries')
119
+ .select('*')
120
+ .gt('id', 100);
121
+
122
+ const { data: lowIds } = await stoorplek.from('countries')
123
+ .select('*')
124
+ .lt('id', 50);
125
+
126
+ // Pattern matching (case-insensitive)
127
+ const { data: unitedCountries } = await stoorplek.from('countries')
128
+ .select('*')
129
+ .ilike('name', '%united%');
130
+
131
+ // Pattern matching (case-sensitive)
132
+ const { data: exactMatch } = await stoorplek.from('countries')
133
+ .select('*')
134
+ .like('name', 'United%');
135
+
136
+ // In array
137
+ const { data: specific } = await stoorplek.from('countries')
138
+ .select('*')
139
+ .in('iso', ['US', 'CA', 'MX']);
140
+
141
+ // Not in array
142
+ const { data: excluded } = await stoorplek.from('countries')
143
+ .select('*')
144
+ .notIn('iso', ['US', 'CA']);
145
+
146
+ // Is null / Is not null
147
+ const { data: withNull } = await stoorplek.from('countries')
148
+ .select('*')
149
+ .isNull('deleted_at');
150
+
151
+ const { data: withoutNull } = await stoorplek.from('countries')
152
+ .select('*')
153
+ .isNotNull('email');
154
+ ```
155
+
156
+ ### Sorting
157
+
158
+ ```javascript
159
+ // Single column sort (ascending by default)
160
+ const { data: sorted } = await stoorplek.from('countries')
161
+ .select('*')
162
+ .order('name');
163
+
164
+ // Descending order
165
+ const { data: sortedDesc } = await stoorplek.from('countries')
166
+ .select('*')
167
+ .order('name', 'desc');
168
+
169
+ // Multiple columns (chain multiple order calls)
170
+ const { data: multiSort } = await stoorplek.from('countries')
171
+ .select('*')
172
+ .order('name', 'asc')
173
+ .order('id', 'desc');
174
+ ```
175
+
176
+ ### Pagination
177
+
178
+ ```javascript
179
+ // Limit results
180
+ const { data: limited } = await stoorplek.from('countries')
181
+ .select('*')
182
+ .limit(10);
183
+
184
+ // Offset for pagination
185
+ const { data: page2 } = await stoorplek.from('countries')
186
+ .select('*')
187
+ .limit(10)
188
+ .offset(10);
189
+
190
+ // Combined with sorting
191
+ const { data: paginatedSorted } = await stoorplek.from('countries')
192
+ .select('*')
193
+ .order('name')
194
+ .limit(20)
195
+ .offset(0);
196
+ ```
197
+
198
+ ### Complex Queries
199
+
200
+ Combine multiple filters for powerful queries:
201
+
202
+ ```javascript
203
+ const { data, error } = await stoorplek.from('countries')
204
+ .select('name, iso, cities(id, name), languages(*)')
205
+ .ilike('name', '%united%')
206
+ .order('name', 'asc')
207
+ .limit(10);
208
+ ```
209
+
210
+ ### Getting Single Result
211
+
212
+ ```javascript
213
+ // Get single result (automatically limits to 1)
214
+ const { data: usa, error } = await stoorplek.from('countries')
215
+ .select('*')
216
+ .eq('iso', 'US')
217
+ .single();
218
+ ```
219
+
220
+ ## TypeScript Support
221
+
222
+ The SDK provides full type safety with automatic type inference:
223
+
224
+ ```typescript
225
+ import { stoorplek } from 'stoorplek-data-api';
226
+
227
+ // TypeScript automatically knows this returns ApiResponse<Country[]>
228
+ const { data, error } = await stoorplek.from('countries').select('*');
229
+
230
+ // Full autocomplete and type checking
231
+ data?.forEach(country => {
232
+ console.log(country.name); // ✅ TypeScript knows 'name' exists
233
+ console.log(country.iso); // ✅ TypeScript knows 'iso' exists
234
+ });
235
+ ```
236
+
237
+ ### Custom Types
238
+
239
+ For dynamic tables or custom type definitions:
240
+
241
+ ```typescript
242
+ interface CustomType {
243
+ id: number;
244
+ customField: string;
245
+ }
246
+
247
+ const { data, error } = await stoorplek.from<CustomType>('custom_table').select('*');
248
+ // data is typed as CustomType[] | null
249
+ ```
250
+
251
+ ## Error Handling
252
+
253
+ ```javascript
254
+ import { stoorplek } from 'stoorplek-data-api';
255
+
256
+ const { data, error } = await stoorplek.from('countries')
257
+ .select('*')
258
+ .eq('iso', 'US');
259
+
260
+ if (error) {
261
+ console.error('API Error:', error.message);
262
+ console.error('Error Code:', error.code);
263
+ } else {
264
+ console.log('Data:', data);
265
+ }
266
+ ```
267
+
268
+ ## API Reference
269
+
270
+ ### `createClient(options)`
271
+
272
+ Creates a new API client instance.
273
+
274
+ **Parameters:**
275
+ - `options.apiKey` (required): Your API key
276
+ - `options.baseUrl` (optional): Base URL for the API (defaults to http://localhost:3000)
277
+ - `options.timeout` (optional): Request timeout in milliseconds (defaults to 30000)
278
+
279
+ **Returns:** `ApiClient` instance
280
+
281
+ **Example:**
282
+ ```javascript
283
+ const client = createClient({
284
+ apiKey: 'sk_live_your_api_key',
285
+ baseUrl: 'https://api.yourservice.com',
286
+ timeout: 30000
287
+ });
288
+ ```
289
+
290
+ ### `configure(options)`
291
+
292
+ Configure the default client for one-liner usage.
293
+
294
+ **Parameters:**
295
+ - `options.apiKey` (required): Your API key
296
+ - `options.baseUrl` (optional): Base URL for the API
297
+ - `options.timeout` (optional): Request timeout in milliseconds
298
+
299
+ **Example:**
300
+ ```javascript
301
+ import { configure } from 'stoorplek-data-api';
302
+
303
+ configure({
304
+ apiKey: process.env.MY_API_KEY,
305
+ timeout: 30000
306
+ });
307
+ ```
308
+
309
+ ### `from(table)` / `query(table)`
310
+
311
+ Start a chainable query for a specific table.
312
+
313
+ **Parameters:**
314
+ - `table`: Name of the table to query
315
+
316
+ **Returns:** `QueryBuilder<T>` instance with chainable methods
317
+
318
+ **Chainable Methods:**
319
+ - `.select(query)` - **Required.** Select columns and relations using Supabase-style syntax (e.g., `'id, name, cities(id, name)'`)
320
+ - **Filters:** `.eq()`, `.neq()`, `.gt()`, `.gte()`, `.lt()`, `.lte()`, `.like()`, `.ilike()`, `.in()`, `.notIn()`, `.isNull()`, `.isNotNull()` - See [Filtering](#filtering) section for examples
321
+ - `.order(column, direction?)` - Sort (direction: 'asc' | 'desc', defaults to 'asc')
322
+ - `.limit(count)` - Limit results
323
+ - `.offset(count)` - Pagination offset
324
+ - `.range(from, to)` - Set offset and limit together
325
+ - `.single()` - Execute query and return single result (returns object instead of array)
326
+
327
+ **Example:**
328
+ ```javascript
329
+ const { data, error } = await stoorplek.from('countries')
330
+ .select('name, iso, cities(id, name)')
331
+ .ilike('name', '%united%')
332
+ .order('name', 'asc')
333
+ .limit(10);
334
+ ```
335
+
336
+ ### `health()`
337
+
338
+ Check if the API is healthy.
339
+
340
+ **Returns:** `Promise<ApiResponse>`
341
+
342
+ **Example:**
343
+ ```javascript
344
+ import { health } from 'stoorplek-data-api';
345
+
346
+ const status = await health();
347
+ console.log(status);
348
+ ```
349
+
350
+ ### `stoorplek`
351
+
352
+ Default export object for easy importing and usage.
353
+
354
+ **Example:**
355
+ ```javascript
356
+ const { stoorplek } = require('stoorplek-data-api');
357
+
358
+ // Configure (or use MY_API_KEY env var)
359
+ stoorplek.configure({ apiKey: 'your-key' });
360
+
361
+ // Use in queries
362
+ const { data, error } = await stoorplek.from('countries').select('*');
363
+ ```
364
+
365
+ ## Response Format
366
+
367
+ All API responses follow this structure:
368
+
369
+ ```typescript
370
+ interface ApiResponse<T> {
371
+ data: T;
372
+ error: {
373
+ message: string;
374
+ code?: string;
375
+ details?: any;
376
+ } | null;
377
+ status: number;
378
+ }
379
+ ```
380
+
381
+ **Example success response:**
382
+ ```json
383
+ {
384
+ "data": [
385
+ { "id": 1, "name": "United States", "iso": "US" },
386
+ { "id": 2, "name": "Canada", "iso": "CA" }
387
+ ],
388
+ "error": null,
389
+ "status": 200
390
+ }
391
+ ```
392
+
393
+ **Example error response:**
394
+ ```json
395
+ {
396
+ "data": [],
397
+ "error": {
398
+ "message": "Invalid table name",
399
+ "code": "INVALID_TABLE",
400
+ "details": {}
401
+ },
402
+ "status": 400
403
+ }
404
+ ```
405
+
406
+ ## Advanced Usage
407
+
408
+ ### Using with the Client Instance
409
+
410
+ ```javascript
411
+ import { createClient } from 'stoorplek-data-api';
412
+
413
+ const client = createClient({
414
+ apiKey: process.env.API_KEY
415
+ });
416
+
417
+ // All the same chainable methods work
418
+ const { data, error } = await client
419
+ .from('countries')
420
+ .select('name, iso')
421
+ .ilike('name', '%united%');
422
+ ```
423
+
424
+ ### Environment Variables
425
+
426
+ The SDK automatically looks for `MY_API_KEY` in your environment:
427
+
428
+ ```env
429
+ # .env file
430
+ MY_API_KEY=sk_live_your_api_key_here
431
+ ```
432
+
433
+ Then just import and use:
434
+ ```javascript
435
+ import { stoorplek } from 'stoorplek-data-api';
436
+
437
+ const { data, error } = await stoorplek.from('countries').select('*'); // No configuration needed!
438
+ ```
439
+
440
+ ## Available Tables
441
+
442
+ Currently supported tables:
443
+ - `countries` - Country data with relations to `cities` and `languages`
444
+ - `cities` - City data with relation to `country`
445
+ - `languages` - Language data with relation to `countries`
446
+
447
+ More tables will be added as the API expands.
448
+
449
+ ## License
450
+
451
+ MIT
@@ -0,0 +1,33 @@
1
+ import { ApiClientOptions, ApiResponse, QueryBuilder, TableTypes } from './types';
2
+ /**
3
+ * Main API client class
4
+ */
5
+ export declare class ApiClient {
6
+ private apiKey;
7
+ private baseUrl;
8
+ private timeout;
9
+ constructor(options: ApiClientOptions);
10
+ /**
11
+ * Start a chainable query for a table
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const countries = await client
16
+ * .from('countries')
17
+ * .select('name, iso, cities(id, name, population)')
18
+ * .ilike('name', '%united%')
19
+ * .order('name', 'asc')
20
+ * .limit(10);
21
+ * ```
22
+ */
23
+ from<T extends keyof TableTypes>(table: T): QueryBuilder<TableTypes[T]>;
24
+ from<T = any>(table: string): QueryBuilder<T>;
25
+ /**
26
+ * Handles common error cases and returns appropriate ApiResponse
27
+ */
28
+ private handleError;
29
+ private executeQuery;
30
+ health(): Promise<ApiResponse>;
31
+ private validateTable;
32
+ }
33
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAc,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAK9F;;GAEG;AACH,qBAAa,SAAS;IAClB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,gBAAgB;IAcrC;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,UAAU,EAAE,KAAK,EAAE,CAAC,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACvE,IAAI,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC;IAa7C;;OAEG;IACH,OAAO,CAAC,WAAW;YA4BL,YAAY;IA6DpB,MAAM,IAAI,OAAO,CAAC,WAAW,CAAC;IAsCpC,OAAO,CAAC,aAAa;CAOxB"}
package/dist/client.js ADDED
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiClient = void 0;
4
+ const api_1 = require("./types/api");
5
+ const constants_1 = require("./constants");
6
+ const query_builder_1 = require("./utils/query-builder");
7
+ /**
8
+ * Main API client class
9
+ */
10
+ class ApiClient {
11
+ constructor(options) {
12
+ this.apiKey = options.apiKey;
13
+ this.baseUrl = options.baseUrl || constants_1.DEFAULT_BASE_URL;
14
+ this.timeout = options.timeout || constants_1.DEFAULT_TIMEOUT;
15
+ if (!this.apiKey) {
16
+ throw new Error('API key is required');
17
+ }
18
+ }
19
+ from(table) {
20
+ this.validateTable(table);
21
+ return new query_builder_1.QueryBuilderImpl(table, (t, state) => this.executeQuery(t, state));
22
+ }
23
+ // ===========================================
24
+ // ERROR HANDLING
25
+ // ===========================================
26
+ /**
27
+ * Handles common error cases and returns appropriate ApiResponse
28
+ */
29
+ handleError(error, defaultMessage, defaultData) {
30
+ if (error instanceof Error && error.name === 'AbortError') {
31
+ return {
32
+ data: defaultData,
33
+ error: {
34
+ message: `Request timeout after ${this.timeout}ms`,
35
+ code: 'TIMEOUT',
36
+ },
37
+ status: 408,
38
+ };
39
+ }
40
+ const errorMessage = error instanceof Error ? error.message : defaultMessage;
41
+ return {
42
+ data: defaultData,
43
+ error: {
44
+ message: errorMessage,
45
+ code: 'FETCH_ERROR',
46
+ details: error,
47
+ },
48
+ status: 0,
49
+ };
50
+ }
51
+ // ===========================================
52
+ // DIRECT EXECUTION (Internal)
53
+ // ===========================================
54
+ async executeQuery(table, state) {
55
+ const queryString = (0, query_builder_1.buildQueryString)(state);
56
+ const url = `${this.baseUrl}/data/${table}${queryString}`;
57
+ try {
58
+ const controller = new AbortController();
59
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
60
+ const response = await fetch(url, {
61
+ method: 'GET',
62
+ headers: {
63
+ 'Authorization': `Bearer ${this.apiKey}`,
64
+ 'Content-Type': 'application/json',
65
+ },
66
+ signal: controller.signal,
67
+ });
68
+ clearTimeout(timeoutId);
69
+ const jsonData = await response.json();
70
+ if (!response.ok) {
71
+ const errorData = (0, api_1.isApiErrorResponse)(jsonData) ? jsonData : {};
72
+ return {
73
+ data: [],
74
+ error: {
75
+ message: errorData.message || 'Request failed',
76
+ code: errorData.code,
77
+ details: errorData.details,
78
+ },
79
+ status: response.status,
80
+ };
81
+ }
82
+ if ((0, api_1.isApiSuccessResponse)(jsonData)) {
83
+ return {
84
+ data: jsonData.data,
85
+ error: null,
86
+ status: response.status,
87
+ };
88
+ }
89
+ // Fallback for unexpected response structure
90
+ return {
91
+ data: [],
92
+ error: {
93
+ message: 'Unexpected response format',
94
+ code: 'INVALID_RESPONSE',
95
+ details: jsonData,
96
+ },
97
+ status: response.status,
98
+ };
99
+ }
100
+ catch (error) {
101
+ return this.handleError(error, 'Unknown error occurred', []);
102
+ }
103
+ }
104
+ // ===========================================
105
+ // HEALTH CHECK
106
+ // ===========================================
107
+ async health() {
108
+ const url = `${this.baseUrl}/health`;
109
+ try {
110
+ const controller = new AbortController();
111
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
112
+ const response = await fetch(url, {
113
+ method: 'GET',
114
+ headers: {
115
+ 'Authorization': `Bearer ${this.apiKey}`,
116
+ },
117
+ signal: controller.signal,
118
+ });
119
+ clearTimeout(timeoutId);
120
+ const data = await response.json();
121
+ return {
122
+ data,
123
+ error: null,
124
+ status: response.status,
125
+ };
126
+ }
127
+ catch (error) {
128
+ const response = this.handleError(error, 'Health check failed', null);
129
+ // Override error code for health check
130
+ return {
131
+ ...response,
132
+ error: response.error ? { ...response.error, code: 'HEALTH_CHECK_ERROR' } : null,
133
+ };
134
+ }
135
+ }
136
+ // ===========================================
137
+ // VALIDATION
138
+ // ===========================================
139
+ validateTable(table) {
140
+ if (!constants_1.ALLOWED_TABLES[table]) {
141
+ throw new Error(`Invalid table: "${table}". Allowed tables: ${Object.keys(constants_1.ALLOWED_TABLES).join(', ')}`);
142
+ }
143
+ }
144
+ }
145
+ exports.ApiClient = ApiClient;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Package constants and configuration
3
+ */
4
+ /**
5
+ * List of allowed table names
6
+ * This should match your server's whitelist
7
+ */
8
+ export interface TableDefinition {
9
+ relations?: string[];
10
+ }
11
+ export declare const ALLOWED_TABLES: Record<string, TableDefinition>;
12
+ /**
13
+ * Type for allowed table names
14
+ */
15
+ export type AllowedTable = keyof typeof ALLOWED_TABLES;
16
+ /**
17
+ * Default API base URL
18
+ */
19
+ export declare const DEFAULT_BASE_URL = "http://localhost:3000";
20
+ /**
21
+ * Environment variable name for API key
22
+ */
23
+ export declare const API_KEY_ENV_VAR = "MY_API_KEY";
24
+ export declare const DEFAULT_TIMEOUT = 30000;
25
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC5B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAU1D,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,cAAc,CAAC;AAEvD;;GAEG;AACH,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,eAAe,eAAe,CAAC;AAE5C,eAAO,MAAM,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * Package constants and configuration
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_TIMEOUT = exports.API_KEY_ENV_VAR = exports.DEFAULT_BASE_URL = exports.ALLOWED_TABLES = void 0;
7
+ exports.ALLOWED_TABLES = {
8
+ countries: {
9
+ relations: ['cities', 'languages']
10
+ },
11
+ cities: {
12
+ relations: ['country']
13
+ },
14
+ languages: {
15
+ relations: ['countries']
16
+ }
17
+ };
18
+ /**
19
+ * Default API base URL
20
+ */
21
+ exports.DEFAULT_BASE_URL = 'http://localhost:3000';
22
+ /**
23
+ * Environment variable name for API key
24
+ */
25
+ exports.API_KEY_ENV_VAR = 'MY_API_KEY';
26
+ exports.DEFAULT_TIMEOUT = 30000; // 30 seconds