@taruvi/sdk 1.5.0-beta.1 → 1.5.0-beta.2

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.
Files changed (69) hide show
  1. package/README.md +58 -1295
  2. package/package.json +10 -2
  3. package/.claude/settings.local.json +0 -19
  4. package/.github/worflows/publish.yml +0 -57
  5. package/.github/workflows/publish.yml +0 -58
  6. package/.kiro/settings/lsp.json +0 -198
  7. package/MODULE_NAMING_CHANGES.md +0 -81
  8. package/PARAMETER_NAMING_CHANGES.md +0 -106
  9. package/USAGE_EXAMPLE.md +0 -86
  10. package/src/client.ts +0 -88
  11. package/src/index.ts +0 -51
  12. package/src/lib/analytics/AnalyticsClient.ts +0 -24
  13. package/src/lib/analytics/types.ts +0 -8
  14. package/src/lib/app/AppClient.ts +0 -54
  15. package/src/lib/app/types.ts +0 -50
  16. package/src/lib/auth/AuthClient.ts +0 -126
  17. package/src/lib/auth/types.ts +0 -123
  18. package/src/lib/database/DatabaseClient.ts +0 -306
  19. package/src/lib/database/types.ts +0 -156
  20. package/src/lib/functions/FunctionsClient.ts +0 -27
  21. package/src/lib/functions/types.ts +0 -27
  22. package/src/lib/policy/PolicyClient.ts +0 -79
  23. package/src/lib/policy/types.ts +0 -39
  24. package/src/lib/secrets/SecretsClient.ts +0 -75
  25. package/src/lib/secrets/types.ts +0 -59
  26. package/src/lib/settings/SettingsClient.ts +0 -22
  27. package/src/lib/settings/types.ts +0 -9
  28. package/src/lib/storage/StorageClient.ts +0 -131
  29. package/src/lib/storage/types.ts +0 -86
  30. package/src/lib/users/UserClient.ts +0 -63
  31. package/src/lib/users/types.ts +0 -123
  32. package/src/lib-internal/errors/ErrorClient.ts +0 -114
  33. package/src/lib-internal/errors/index.ts +0 -3
  34. package/src/lib-internal/errors/types.ts +0 -29
  35. package/src/lib-internal/http/HttpClient.ts +0 -116
  36. package/src/lib-internal/http/types.ts +0 -12
  37. package/src/lib-internal/routes/AnalyticsRoutes.ts +0 -3
  38. package/src/lib-internal/routes/AppRoutes.ts +0 -9
  39. package/src/lib-internal/routes/AuthRoutes.ts +0 -0
  40. package/src/lib-internal/routes/DatabaseRoutes.ts +0 -10
  41. package/src/lib-internal/routes/FunctionRoutes.ts +0 -3
  42. package/src/lib-internal/routes/PolicyRoutes.ts +0 -4
  43. package/src/lib-internal/routes/SecretsRoutes.ts +0 -5
  44. package/src/lib-internal/routes/SettingsRoutes.ts +0 -4
  45. package/src/lib-internal/routes/StorageRoutes.ts +0 -15
  46. package/src/lib-internal/routes/UserRoutes.ts +0 -12
  47. package/src/lib-internal/routes/index.ts +0 -0
  48. package/src/lib-internal/token/TokenClient.ts +0 -108
  49. package/src/lib-internal/token/types.ts +0 -0
  50. package/src/types.ts +0 -104
  51. package/src/utils/enums.ts +0 -24
  52. package/src/utils/utils.ts +0 -38
  53. package/tests/fixtures/mockClient.ts +0 -19
  54. package/tests/mocks/db.json +0 -1
  55. package/tests/unit/analytics/AnalyticsClient.test.ts +0 -84
  56. package/tests/unit/app/AppClient.test.ts +0 -114
  57. package/tests/unit/auth/AuthClient.test.ts +0 -91
  58. package/tests/unit/client/Client.test.ts +0 -87
  59. package/tests/unit/database/DatabaseClient.test.ts +0 -652
  60. package/tests/unit/edge-cases/robustness.test.ts +0 -258
  61. package/tests/unit/errors/errors.test.ts +0 -236
  62. package/tests/unit/functions/FunctionsClient.test.ts +0 -99
  63. package/tests/unit/policy/PolicyClient.test.ts +0 -180
  64. package/tests/unit/secrets/SecretsClient.test.ts +0 -146
  65. package/tests/unit/settings/SettingsClient.test.ts +0 -50
  66. package/tests/unit/storage/StorageClient.test.ts +0 -252
  67. package/tests/unit/users/UserClient.test.ts +0 -150
  68. package/tsconfig.json +0 -44
  69. package/vitest.config.ts +0 -7
package/README.md CHANGED
@@ -1,1329 +1,92 @@
1
- # Taruvi SDK - AI Implementation Guide
1
+ # Taruvi SDK
2
2
 
3
- > Quick reference for implementing Taruvi SDK features with copy-paste examples from production code
3
+ TypeScript SDK for the Taruvi platform. It gives developers a **consistent, typed way to query the Taruvi backend** — shared configuration, session authentication, fluent query builders, and structured errors across data, storage, auth, users, functions, analytics, policy, secrets, and app services.
4
+
5
+ **Package:** `@taruvi/sdk` · **Version:** 1.5.0-beta.1 · **License:** MIT
6
+
7
+ **Branches:** `main` is for **stable** releases; `beta` is for **experimental** releases. Bumping `version` in `package.json` and pushing to either branch triggers CI/CD (`npm test`, then publish to npm with the matching tag). See [Releases and branches](docs/08-releases-and-branches.md).
4
8
 
5
9
  ## Installation
6
10
 
7
11
  ```bash
12
+ # Stable (from main)
8
13
  npm install @taruvi/sdk
9
- ```
10
-
11
- ## What's New
12
-
13
- Recent updates to the SDK:
14
-
15
- - **Analytics Service**: New service for executing analytics queries with `execute()` method. Pass query name and parameters to run predefined analytics.
16
- - **Policy Service**: New service for checking resource-level permissions with `checkResource()` method. Supports batch resource checking with entity types, tables, and attributes.
17
- - **App Service**: New service to retrieve app roles with `roles()` method.
18
- - **User Types**: Added `UserCreateRequest`, `UserResponse`, and `UserDataResponse` types for user management.
19
- - **Auth Service**: Web UI Flow with `login()`, `signup()`, `logout()` methods that redirect to backend pages. Token refresh with rotation support, `getCurrentUser()` for JWT decoding.
20
- - **Database Service**: Added `create()` method for creating records. Added `populate()` method for eager loading related records. Comprehensive filter support with Django-style operators (`__gte`, `__lte`, `__icontains`, etc.). Use **`orderBy()`** for sorting: one field (`orderBy('created_at', 'desc')`), multiple fields (`orderBy([{ field, order }])`), or a raw `ordering` string (`orderBy('-salary,hire_date')`). Hierarchy traversal uses **`.include('descendants' | 'ancestors' | 'both')`** — not `FilterOperator`. Import **`PgRangeValue`** for typed PG range columns in row data.
21
- - **Storage Service**: Added `download()` method. Enhanced filter support with size, date, MIME type, visibility filters. `delete()` now accepts array of paths for bulk deletion.
22
- - **Client**: Automatic token extraction from URL hash after OAuth callback - no manual token handling needed.
23
- - **Types**: Comprehensive `StorageFilters` and `DatabaseFilters` interfaces with full operator support.
24
-
25
- ## Core Setup
26
-
27
- ### 1. Initialize Client
28
-
29
- ```typescript
30
- import { Client } from '@taruvi/sdk'
31
-
32
- const taruviClient = new Client({
33
- apiKey: "your-site-api-key",
34
- appSlug: "your-app-slug",
35
- apiUrl: "https://taruvi-site.taruvi.cloud"
36
- })
37
- ```
38
-
39
- ### 2. Pass to Components (React)
40
-
41
- ```typescript
42
- // App.tsx
43
- <Route path="/page" element={<MyPage taruviClient={taruviClient} />} />
44
-
45
- // MyPage.tsx
46
- interface MyPageProps {
47
- taruviClient: Client;
48
- }
49
-
50
- export default function MyPage({ taruviClient }: MyPageProps) {
51
- // Use SDK here
52
- }
53
- ```
54
-
55
- ### 3. Automatic Token Handling
56
-
57
- The Client automatically extracts authentication tokens from URL hash after OAuth callback:
58
-
59
- ```typescript
60
- // After login redirect, URL contains:
61
- // #session_token=xxx&access_token=yyy&refresh_token=zzz&expires_in=172800&token_type=Bearer
62
-
63
- // Client automatically:
64
- // 1. Extracts tokens from URL hash
65
- // 2. Stores them in TokenClient
66
- // 3. Clears the hash from URL
67
-
68
- const taruviClient = new Client({ apiKey, appSlug, baseUrl })
69
- // Tokens are now available automatically if present in URL hash
70
- ```
71
-
72
- ---
73
-
74
- ## Auth Service
75
-
76
- ### Check Authentication
77
-
78
- ```typescript
79
- import { Auth } from '@taruvi/sdk'
80
-
81
- const auth = new Auth(taruviClient)
82
- const isAuthenticated = auth.isUserAuthenticated() // Returns boolean (synchronous)
83
- ```
84
-
85
- ### Login Flow (Web UI Flow with Redirect)
86
-
87
- ```typescript
88
- import { useEffect } from "react"
89
- import { Auth } from '@taruvi/sdk'
90
-
91
- export default function Login({ taruviClient }) {
92
- const auth = new Auth(taruviClient)
93
-
94
- useEffect(() => {
95
- const isAuthenticated = auth.isUserAuthenticated()
96
-
97
- if (isAuthenticated) {
98
- window.location.href = "/dashboard"
99
- } else {
100
- // Redirects to backend login page, then returns with tokens in URL hash
101
- auth.login() // Optional: pass callback URL
102
- }
103
- }, [])
104
-
105
- return <div>Checking authentication...</div>
106
- }
107
- ```
108
-
109
- ### Signup Flow
110
-
111
- ```typescript
112
- import { Auth } from '@taruvi/sdk'
113
-
114
- const auth = new Auth(taruviClient)
115
-
116
- // Redirect to signup page (Web UI Flow)
117
- auth.signup() // Optional: pass callback URL
118
- auth.signup("/dashboard") // Redirect to dashboard after signup
119
- ```
120
-
121
- ### Logout
122
14
 
123
- ```typescript
124
- import { Auth } from '@taruvi/sdk'
125
-
126
- const auth = new Auth(taruviClient)
127
-
128
- // Clear tokens and redirect to logout page
129
- await auth.logout() // Optional: pass callback URL
130
- await auth.logout("/") // Redirect to home after logout
131
- ```
132
-
133
- ### Get Current User
134
-
135
- ```typescript
136
- import { Auth } from '@taruvi/sdk'
137
-
138
- const auth = new Auth(taruviClient)
139
-
140
- // Get user info from decoded JWT access token
141
- const user = auth.getCurrentUser()
142
- console.log(user?.user_id)
143
- console.log(user?.username)
144
- console.log(user?.email)
145
- ```
146
-
147
- ### Token Management
148
-
149
- ```typescript
150
- import { Auth } from '@taruvi/sdk'
151
-
152
- const auth = new Auth(taruviClient)
153
-
154
- // Get tokens
155
- const accessToken = auth.getAccessToken()
156
- const refreshToken = auth.getRefreshToken()
157
-
158
- // Check if token is expired
159
- const isExpired = auth.isTokenExpired()
160
-
161
- // Refresh access token (returns new access AND refresh tokens due to rotation)
162
- const newTokens = await auth.refreshAccessToken()
163
- if (newTokens) {
164
- console.log(newTokens.access)
165
- console.log(newTokens.refresh)
166
- console.log(newTokens.expires_in)
167
- }
168
- ```
169
-
170
- ---
171
-
172
- ## User Service
173
-
174
- ### Get Current User
175
-
176
- ```typescript
177
- import { User } from '@taruvi/sdk'
178
-
179
- const user = new User(taruviClient)
180
- const userData = await user.getUserData()
181
-
182
- console.log(userData.data.username)
183
- console.log(userData.data.email)
184
- console.log(userData.data.full_name)
185
- ```
186
-
187
- ### Create User (Registration)
188
-
189
- ```typescript
190
- import { User } from '@taruvi/sdk'
191
-
192
- const user = new User(taruviClient)
193
-
194
- const newUser = await user.createUser({
195
- username: "john_doe",
196
- email: "john@example.com",
197
- password: "secure123",
198
- confirm_password: "secure123",
199
- first_name: "John",
200
- last_name: "Doe",
201
- is_active: true,
202
- is_staff: false,
203
- attributes: ""
204
- })
205
- ```
206
-
207
- ### Update User
208
-
209
- ```typescript
210
- const user = new User(taruviClient)
211
-
212
- await user.updateUser("john_doe", {
213
- email: "newemail@example.com",
214
- first_name: "Johnny"
215
- })
216
- ```
217
-
218
- ### Delete User
219
-
220
- ```typescript
221
- const user = new User(taruviClient)
222
- await user.deleteUser("john_doe")
15
+ # Experimental (from beta)
16
+ npm install @taruvi/sdk@beta
223
17
  ```
224
18
 
225
- ### List Users
19
+ **Peer dependencies:** `axios` (>=1), `typescript` (>=5.7), optional `@types/node` for server use.
226
20
 
227
- ```typescript
228
- const user = new User(taruviClient)
229
-
230
- const users = await user.list({
231
- search: "john",
232
- is_active: true,
233
- is_staff: false,
234
- is_superuser: false,
235
- is_deleted: false,
236
- ordering: "-date_joined",
237
- page: 1,
238
- page_size: 20
239
- })
240
- ```
241
-
242
- ### Get User Apps
21
+ ## Quick start
243
22
 
244
23
  ```typescript
245
- const user = new User(taruviClient)
246
- const apps = await user.getUserApps("john_doe")
24
+ import { Client, Database } from '@taruvi/sdk'
247
25
 
248
- // Returns array of apps the user has access to
249
- apps.forEach(app => {
250
- console.log(app.name, app.slug, app.url)
26
+ const client = new Client({
27
+ apiKey: 'your-site-api-key',
28
+ appSlug: 'your-app-slug',
29
+ apiUrl: 'https://taruvi-site.taruvi.cloud',
251
30
  })
252
- ```
253
-
254
- ### Complete Registration Form Example
255
-
256
- ```typescript
257
- import { useState } from "react"
258
- import { User } from "@taruvi/sdk"
259
- import { useNavigate } from "react-router"
260
-
261
- export default function Register({ taruviClient }) {
262
- const navigate = useNavigate()
263
- const [formData, setFormData] = useState({
264
- username: "",
265
- email: "",
266
- password: "",
267
- confirm_password: "",
268
- first_name: "",
269
- last_name: "",
270
- is_active: true,
271
- is_staff: false
272
- })
273
- const [error, setError] = useState("")
274
- const [loading, setLoading] = useState(false)
275
-
276
- const handleSubmit = async (e) => {
277
- e.preventDefault()
278
-
279
- if (formData.password !== formData.confirm_password) {
280
- setError("Passwords do not match")
281
- return
282
- }
283
-
284
- setLoading(true)
285
- try {
286
- const userClient = new User(taruviClient)
287
- await userClient.createUser(formData)
288
- navigate("/login")
289
- } catch (err) {
290
- setError(err.message || "Failed to create user")
291
- } finally {
292
- setLoading(false)
293
- }
294
- }
295
-
296
- return (
297
- <form onSubmit={handleSubmit}>
298
- <input
299
- name="username"
300
- value={formData.username}
301
- onChange={(e) => setFormData({...formData, username: e.target.value})}
302
- required
303
- />
304
- {/* Add other fields */}
305
- <button type="submit" disabled={loading}>
306
- {loading ? "Creating..." : "Register"}
307
- </button>
308
- {error && <div>{error}</div>}
309
- </form>
310
- )
311
- }
312
- ```
313
-
314
- ---
315
-
316
- ## Database Service (CRUD Operations)
317
-
318
- ### Fetch All Records
319
-
320
- ```typescript
321
- import { Database } from '@taruvi/sdk'
322
-
323
- const db = new Database(taruviClient)
324
- const response = await db.from("accounts").execute()
325
-
326
- if (response.data) {
327
- console.log(response.data) // Array of records
328
- }
329
- ```
330
-
331
- ### Fetch Single Record
332
-
333
- ```typescript
334
- const db = new Database(taruviClient)
335
- const record = await db.from("accounts").get("record-id").execute()
336
- ```
337
-
338
- ### Create Record
339
-
340
- ```typescript
341
- const db = new Database(taruviClient)
342
- await db.from("accounts").create({
343
- name: "John Doe",
344
- email: "john@example.com",
345
- status: "active"
346
- }).execute()
347
- ```
348
-
349
- ### Update Record
350
-
351
- ```typescript
352
- const db = new Database(taruviClient)
353
- await db.from("accounts").get("record-id").update({
354
- name: "Updated Name",
355
- status: "active"
356
- }).execute()
357
- ```
358
-
359
- ### Delete Record
360
-
361
- ```typescript
362
- const db = new Database(taruviClient)
363
- await db.from("accounts").delete("record-id").execute()
364
- ```
365
-
366
- ### Filter records
367
-
368
- `Database` supports **DRF-style flat filters** (`filters(field, operator, value)`) and a **JSON filter tree** sent as the `filters` query param (`filters(tree)`).
369
-
370
- ```typescript
371
- const db = new Database(taruviClient)
372
-
373
- // Flat: one condition per call (chain for AND)
374
- const filtered = await db
375
- .from("accounts")
376
- .filters("status", "eq", "active")
377
- .filters("country", "eq", "USA")
378
- .execute()
379
31
 
380
- // Flat: operators map to `field__suffix` query keys
381
- const advanced = await db
382
- .from("accounts")
383
- .filters("age", "gte", 18)
384
- .filters("age", "lt", 65)
385
- .filters("name", "icontains", "john")
386
- .filters("created_at", "gte", "2024-01-01")
387
- .orderBy("created_at", "desc")
388
- .execute()
32
+ const db = new Database(client)
389
33
 
390
- // Pagination (separate methods)
391
- const paginated = await db
392
- .from("accounts")
34
+ const response = await db
35
+ .from('accounts')
36
+ .filters('status', 'eq', 'active')
37
+ .orderBy('created_at', 'desc')
393
38
  .page(1)
394
39
  .pageSize(20)
395
40
  .execute()
396
-
397
- // JSON tree → `?filters=<url-encoded JSON>` (e.g. from Refine / your UI)
398
- import type { BackendFilterTreeRoot } from "@taruvi/sdk"
399
- const tree: BackendFilterTreeRoot = [
400
- {
401
- operator: "and",
402
- value: [
403
- { field: "is_active", operator: "eq", value: true },
404
- { field: "hire_date", operator: "lt", value: "2021-01-01" },
405
- ],
406
- },
407
- ]
408
- await db.from("employees").filters(tree).execute()
409
- ```
410
-
411
- ### Populate related records
412
-
413
- Use `populate()` to eager load related records (foreign key relationships):
414
-
415
- ```typescript
416
- const db = new Database(taruviClient)
417
-
418
- // Populate a single relation
419
- const orders = await db
420
- .from("orders")
421
- .populate(["customer"])
422
- .execute()
423
-
424
- // Each order now includes the full customer object instead of just the ID
425
- console.log(orders.data[0].customer.name)
426
- console.log(orders.data[0].customer.email)
427
-
428
- // Populate multiple relations
429
- const invoices = await db
430
- .from("invoices")
431
- .populate(["customer", "created_by", "items"])
432
- .execute()
433
-
434
- // Combine with flat filters + populate
435
- const recentOrders = await db
436
- .from("orders")
437
- .filters("status", "eq", "completed")
438
- .filters("created_at", "gte", "2024-01-01")
439
- .orderBy("created_at", "desc")
440
- .populate(["customer", "product"])
441
- .execute()
442
-
443
- // Combine with pagination + populate
444
- const paginatedOrders = await db
445
- .from("orders")
446
- .page(1)
447
- .pageSize(10)
448
- .populate(["customer"])
449
- .execute()
450
- ```
451
-
452
- ### Complete CRUD Example (CRM Table)
453
-
454
- ```typescript
455
- import { useEffect, useState } from 'react'
456
- import { Database } from '@taruvi/sdk'
457
-
458
- export default function CrmTable({ taruviClient }) {
459
- const [contacts, setContacts] = useState([])
460
-
461
- const fetchContacts = async () => {
462
- const db = new Database(taruviClient)
463
- const response = await db.from("accounts").execute()
464
- if (response.data) {
465
- setContacts(response.data)
466
- }
467
- }
468
-
469
- useEffect(() => {
470
- fetchContacts()
471
- }, [])
472
-
473
- const handleCreate = async (data) => {
474
- const db = new Database(taruviClient)
475
- await db.from("accounts").create(data).execute()
476
- fetchContacts() // Refresh
477
- }
478
-
479
- const handleDelete = async (id) => {
480
- const db = new Database(taruviClient)
481
- await db.from("accounts").delete(id).execute()
482
- fetchContacts() // Refresh
483
- }
484
-
485
- const handleUpdate = async (id, data) => {
486
- const db = new Database(taruviClient)
487
- await db.from("accounts").get(id).update(data).execute()
488
- fetchContacts() // Refresh
489
- }
490
-
491
- return (
492
- <div>
493
- <button onClick={() => handleCreate({ name: 'New Contact', status: 'active' })}>
494
- Add Contact
495
- </button>
496
- <table>
497
- {contacts.map(contact => (
498
- <tr key={contact.id}>
499
- <td>{contact.name}</td>
500
- <td>
501
- <button onClick={() => handleUpdate(contact.id, { status: 'updated' })}>
502
- Edit
503
- </button>
504
- <button onClick={() => handleDelete(contact.id)}>
505
- Delete
506
- </button>
507
- </td>
508
- </tr>
509
- ))}
510
- </table>
511
- </div>
512
- )
513
- }
514
- ```
515
-
516
- ---
517
-
518
- ## Storage Service (File Management)
519
-
520
- ### List Files in Bucket
521
-
522
- ```typescript
523
- import { Storage } from '@taruvi/sdk'
524
-
525
- const storage = new Storage(taruviClient)
526
- const files = await storage.from("documents").execute()
527
-
528
- console.log(files.data) // Array of file objects
529
- ```
530
-
531
- ### Upload Files
532
-
533
- ```typescript
534
- const storage = new Storage(taruviClient)
535
-
536
- const filesData = {
537
- files: [file1, file2], // File objects from input
538
- metadatas: [{ name: "file1" }, { name: "file2" }],
539
- paths: ["file1.pdf", "file2.pdf"]
540
- }
541
-
542
- await storage.from("documents").upload(filesData).execute()
543
- ```
544
-
545
- ### Download File
546
-
547
- ```typescript
548
- const storage = new Storage(taruviClient)
549
- const file = await storage.from("documents").download("path/to/file.pdf").execute()
550
- ```
551
-
552
- ### Delete Files
553
-
554
- ```typescript
555
- const storage = new Storage(taruviClient)
556
-
557
- // Delete single file
558
- await storage.from("documents").delete(["path/to/file.pdf"]).execute()
559
-
560
- // Delete multiple files
561
- await storage.from("documents").delete([
562
- "path/to/file1.pdf",
563
- "path/to/file2.pdf",
564
- "path/to/file3.pdf"
565
- ]).execute()
566
41
  ```
567
42
 
568
- ### Update File Metadata
43
+ ## Documentation
569
44
 
570
- ```typescript
571
- const storage = new Storage(taruviClient)
572
- await storage
573
- .from("documents")
574
- .update("path/to/file.pdf", {
575
- visibility: "public",
576
- metadata: { category: "reports" }
577
- })
578
- .execute()
579
- ```
45
+ Full guides live in **[docs/](docs/README.md)**:
580
46
 
581
- ### Filter Files
47
+ | Guide | Topic |
48
+ |-------|--------|
49
+ | [Introduction](docs/01-introduction.md) | Purpose, setup, DI, backend-handled login flow |
50
+ | [Builder pattern](docs/02-builder-pattern.md) | Immutable queries and execution |
51
+ | [Architecture](docs/03-architecture.md) | `lib` vs `lib-internal`, request flow |
52
+ | [Clients](docs/04-clients.md) | What each service does |
53
+ | [API reference](docs/05-api-reference.md) | All public methods |
54
+ | [Examples](docs/06-examples.md) | Chaining patterns and workflows |
55
+ | [Advanced & troubleshooting](docs/07-advanced-topics.md) | Typing, filters, CORS, packaging |
56
+ | [Releases & branches](docs/08-releases-and-branches.md) | Stable vs beta, CI/CD publish workflow |
582
57
 
583
- ```typescript
584
- const storage = new Storage(taruviClient)
58
+ ## Services at a glance
585
59
 
586
- // Basic filters
587
- const filtered = await storage
588
- .from("documents")
589
- .filter({
590
- search: "invoice",
591
- visibility: "public",
592
- mimetype_category: "document",
593
- min_size: 1024,
594
- ordering: "-created_at"
595
- })
596
- .execute()
60
+ | Client | Use for |
61
+ |--------|---------|
62
+ | `Database` | Table CRUD, filters, pagination, graph/edges |
63
+ | `Storage` | Bucket files: list, upload, download, delete |
64
+ | `Auth` | Browser login/signup/logout, session token |
65
+ | `User` | User admin, roles, preferences |
66
+ | `Functions` | Invoke serverless functions |
67
+ | `Analytics` | Run predefined analytics queries |
68
+ | `Settings` | Site metadata, user attribute schema |
69
+ | `Secrets` | Read secret values |
70
+ | `Policy` | Permission checks |
71
+ | `App` | App roles and settings |
597
72
 
598
- // Advanced filters with operators
599
- const advanced = await storage
600
- .from("documents")
601
- .filter({
602
- // Size filters (bytes)
603
- size__gte: 1024, // >= 1KB
604
- size__lte: 10485760, // <= 10MB
73
+ ## What's included
605
74
 
606
- // Date filters (ISO 8601)
607
- created_at__gte: "2024-01-01",
608
- created_at__lte: "2024-12-31",
75
+ - **Immutable builder pattern** for `Database`, `Storage`, `App`, and `Secrets.get()` — each chain step returns a new instance so parallel queries never overwrite each other's URLs or filters.
76
+ - **Session authentication** via `X-Session-Token`; automatic token extraction from URL hash after OAuth redirect in the browser.
77
+ - **Typed errors** — `AuthError`, `NotFoundError`, `ValidationError`, and others exported from the package.
609
78
 
610
- // Search filters
611
- filename__icontains: "report",
612
- file__startswith: "invoice",
613
- prefix: "uploads/2024/",
79
+ ## Development
614
80
 
615
- // MIME type filters
616
- mimetype: "application/pdf",
617
- mimetype_category: "image", // image, document, video, audio, etc.
618
-
619
- // Visibility & user filters
620
- visibility: "public",
621
- created_by_me: true,
622
- created_by__username: "john",
623
-
624
- // Pagination & sorting
625
- page: 1,
626
- pageSize: 50,
627
- ordering: "-created_at" // Sort by created_at descending
628
- })
629
- .execute()
630
- ```
631
-
632
- ### Complete File Upload Example
633
-
634
- ```typescript
635
- import { useState, useRef } from 'react'
636
- import { Storage } from '@taruvi/sdk'
637
-
638
- export default function FileUploader({ taruviClient }) {
639
- const [files, setFiles] = useState([])
640
- const [uploading, setUploading] = useState(false)
641
- const fileInputRef = useRef(null)
642
-
643
- const handleFileSelect = (e) => {
644
- const selectedFiles = Array.from(e.target.files)
645
- setFiles(selectedFiles)
646
- }
647
-
648
- const uploadFiles = async () => {
649
- if (files.length === 0) return
650
-
651
- setUploading(true)
652
- try {
653
- const storage = new Storage(taruviClient)
654
-
655
- const uploadData = {
656
- files: files,
657
- metadatas: files.map(f => ({ name: f.name })),
658
- paths: files.map(f => f.name)
659
- }
660
-
661
- await storage.from("documents").upload(uploadData).execute()
662
-
663
- alert("Upload successful!")
664
- setFiles([])
665
- } catch (error) {
666
- alert("Upload failed: " + error.message)
667
- } finally {
668
- setUploading(false)
669
- }
670
- }
671
-
672
- return (
673
- <div>
674
- <input
675
- ref={fileInputRef}
676
- type="file"
677
- multiple
678
- onChange={handleFileSelect}
679
- />
680
- <button onClick={uploadFiles} disabled={uploading || files.length === 0}>
681
- {uploading ? "Uploading..." : `Upload ${files.length} file(s)`}
682
- </button>
683
- </div>
684
- )
685
- }
686
- ```
687
-
688
- ---
689
-
690
- ## Functions Service (Serverless)
691
-
692
- ### Execute Function (Sync)
693
-
694
- ```typescript
695
- import { Functions } from '@taruvi/sdk'
696
-
697
- const functions = new Functions(taruviClient)
698
-
699
- const result = await functions.execute("my-function", {
700
- async: false,
701
- params: {
702
- key1: "value1",
703
- key2: 123
704
- }
705
- })
706
-
707
- console.log(result.data) // Function response
708
- ```
709
-
710
- ### Execute Function (Async)
711
-
712
- ```typescript
713
- const functions = new Functions(taruviClient)
714
-
715
- const result = await functions.execute("long-running-task", {
716
- async: true,
717
- params: { data: "value" }
718
- })
719
-
720
- console.log(result.invocation.invocation_id) // Track async execution
721
- console.log(result.invocation.celery_task_id)
722
- console.log(result.invocation.status)
723
- ```
724
-
725
- ### Complete Function Executor Example
726
-
727
- ```typescript
728
- import { useState } from 'react'
729
- import { Functions } from '@taruvi/sdk'
730
-
731
- export default function FunctionExecutor({ taruviClient }) {
732
- const [functionSlug, setFunctionSlug] = useState("")
733
- const [params, setParams] = useState({})
734
- const [isAsync, setIsAsync] = useState(false)
735
- const [result, setResult] = useState(null)
736
- const [loading, setLoading] = useState(false)
737
-
738
- const executeFunction = async () => {
739
- setLoading(true)
740
- try {
741
- const functions = new Functions(taruviClient)
742
- const response = await functions.execute(functionSlug, {
743
- async: isAsync,
744
- params: params
745
- })
746
- setResult(response)
747
- } catch (error) {
748
- console.error(error)
749
- } finally {
750
- setLoading(false)
751
- }
752
- }
753
-
754
- return (
755
- <div>
756
- <input
757
- placeholder="Function slug"
758
- value={functionSlug}
759
- onChange={(e) => setFunctionSlug(e.target.value)}
760
- />
761
- <label>
762
- <input
763
- type="checkbox"
764
- checked={isAsync}
765
- onChange={(e) => setIsAsync(e.target.checked)}
766
- />
767
- Async
768
- </label>
769
- <button onClick={executeFunction} disabled={loading}>
770
- Execute
771
- </button>
772
- {result && <pre>{JSON.stringify(result, null, 2)}</pre>}
773
- </div>
774
- )
775
- }
776
- ```
777
-
778
- ---
779
-
780
- ## Analytics Service
781
-
782
- ### Execute Analytics Query
783
-
784
- ```typescript
785
- import { Analytics } from '@taruvi/sdk'
786
-
787
- const analytics = new Analytics(taruviClient)
788
-
789
- const result = await analytics.execute("monthly-sales-report", {
790
- params: {
791
- start_date: "2024-01-01",
792
- end_date: "2024-12-31"
793
- }
794
- })
795
-
796
- console.log(result.data) // Analytics query result
797
- ```
798
-
799
- ### Execute with Typed Response
800
-
801
- ```typescript
802
- interface SalesData {
803
- total_sales: number
804
- orders_count: number
805
- average_order_value: number
806
- }
807
-
808
- const analytics = new Analytics(taruviClient)
809
-
810
- const result = await analytics.execute<SalesData>("sales-summary", {
811
- params: { period: "monthly" }
812
- })
813
-
814
- console.log(result.data?.total_sales)
815
- console.log(result.data?.orders_count)
816
- ```
817
-
818
- ### Complete Analytics Dashboard Example
819
-
820
- ```typescript
821
- import { useEffect, useState } from 'react'
822
- import { Analytics } from '@taruvi/sdk'
823
-
824
- export default function AnalyticsDashboard({ taruviClient }) {
825
- const [metrics, setMetrics] = useState(null)
826
- const [loading, setLoading] = useState(true)
827
-
828
- useEffect(() => {
829
- const fetchMetrics = async () => {
830
- try {
831
- const analytics = new Analytics(taruviClient)
832
- const result = await analytics.execute("dashboard-metrics", {
833
- params: {
834
- date_range: "last_30_days"
835
- }
836
- })
837
- setMetrics(result.data)
838
- } catch (error) {
839
- console.error("Failed to fetch analytics:", error)
840
- } finally {
841
- setLoading(false)
842
- }
843
- }
844
- fetchMetrics()
845
- }, [])
846
-
847
- if (loading) return <div>Loading analytics...</div>
848
-
849
- return (
850
- <div>
851
- <h2>Dashboard Metrics</h2>
852
- <pre>{JSON.stringify(metrics, null, 2)}</pre>
853
- </div>
854
- )
855
- }
856
- ```
857
-
858
- ---
859
-
860
- ## Settings Service
861
-
862
- ### Get Site Settings
863
-
864
- ```typescript
865
- import { Settings } from '@taruvi/sdk'
866
-
867
- const settings = new Settings(taruviClient)
868
- const config = await settings.get()
869
-
870
- console.log(config) // Site configuration object
871
- ```
872
-
873
- ---
874
-
875
- ## Secrets Service
876
-
877
- ### List Secrets
878
-
879
- ```typescript
880
- import { Secrets } from '@taruvi/sdk'
881
-
882
- const secrets = new Secrets(taruviClient)
883
- const result = await secrets.list().execute()
884
-
885
- console.log(result.items) // Array of secrets
886
- ```
887
-
888
- ### Get Secret
889
-
890
- ```typescript
891
- const secrets = new Secrets(taruviClient)
892
- const secret = await secrets.get("MY_SECRET_KEY").execute()
893
-
894
- console.log(secret) // Secret object with value
895
- ```
896
-
897
- ### Update Secret
898
-
899
- ```typescript
900
- const secrets = new Secrets(taruviClient)
901
-
902
- await secrets.update("MY_SECRET", {
903
- value: "my-secret-value"
904
- }).execute()
905
- ```
906
-
907
- ---
908
-
909
- ## Policy Service (Resource Permissions)
910
-
911
- ### Check Resource Permissions
912
-
913
- ```typescript
914
- import { Policy } from '@taruvi/sdk'
915
-
916
- const policy = new Policy(taruviClient)
917
-
918
- // Check permissions for multiple resources
919
- const result = await policy.checkResource([
920
- {
921
- entityType: "crm",
922
- tableName: "accounts",
923
- recordId: "record-123",
924
- attributes: { owner_id: "user-456" },
925
- actions: ["read", "update"]
926
- },
927
- {
928
- entityType: "docs",
929
- tableName: "documents",
930
- recordId: "doc-789",
931
- attributes: {},
932
- actions: ["delete"]
933
- }
934
- ])
935
- ```
936
-
937
- ### Complete Permission Check Example
938
-
939
- ```typescript
940
- import { useState, useEffect } from 'react'
941
- import { Policy } from '@taruvi/sdk'
942
-
943
- export default function ResourceGuard({ taruviClient, children, resource }) {
944
- const [allowed, setAllowed] = useState(false)
945
- const [loading, setLoading] = useState(true)
946
-
947
- useEffect(() => {
948
- const checkPermission = async () => {
949
- try {
950
- const policy = new Policy(taruviClient)
951
- const result = await policy.checkResource([
952
- {
953
- entityType: resource.entityType,
954
- tableName: resource.table,
955
- recordId: resource.id,
956
- attributes: resource.attributes || {},
957
- actions: ["read"]
958
- }
959
- ])
960
- setAllowed(result.allowed)
961
- } catch (error) {
962
- console.error("Permission check failed:", error)
963
- setAllowed(false)
964
- } finally {
965
- setLoading(false)
966
- }
967
- }
968
- checkPermission()
969
- }, [resource])
970
-
971
- if (loading) return <div>Checking permissions...</div>
972
- if (!allowed) return <div>Access denied</div>
973
- return children
974
- }
975
- ```
976
-
977
- ---
978
-
979
- ## App Service
980
-
981
- ### Get App Roles
982
-
983
- ```typescript
984
- import { App } from '@taruvi/sdk'
985
-
986
- const app = new App(taruviClient)
987
- const roles = await app.roles().execute()
988
-
989
- console.log(roles) // Array of role objects with id, name, permissions
990
- ```
991
-
992
- ### Complete Roles List Example
993
-
994
- ```typescript
995
- import { useEffect, useState } from 'react'
996
- import { App } from '@taruvi/sdk'
997
-
998
- export default function RolesList({ taruviClient }) {
999
- const [roles, setRoles] = useState([])
1000
- const [loading, setLoading] = useState(true)
1001
-
1002
- useEffect(() => {
1003
- const fetchRoles = async () => {
1004
- try {
1005
- const app = new App(taruviClient)
1006
- const response = await app.roles().execute()
1007
- setRoles(response.data || [])
1008
- } catch (error) {
1009
- console.error("Failed to fetch roles:", error)
1010
- } finally {
1011
- setLoading(false)
1012
- }
1013
- }
1014
- fetchRoles()
1015
- }, [])
1016
-
1017
- if (loading) return <div>Loading roles...</div>
1018
-
1019
- return (
1020
- <ul>
1021
- {roles.map(role => (
1022
- <li key={role.id}>
1023
- <strong>{role.name}</strong>
1024
- <span>{role.permissions?.join(", ")}</span>
1025
- </li>
1026
- ))}
1027
- </ul>
1028
- )
1029
- }
1030
- ```
1031
-
1032
- ---
1033
-
1034
- ## Common Patterns
1035
-
1036
- ### Loading States
1037
-
1038
- ```typescript
1039
- const [data, setData] = useState(null)
1040
- const [loading, setLoading] = useState(true)
1041
- const [error, setError] = useState(null)
1042
-
1043
- useEffect(() => {
1044
- const fetchData = async () => {
1045
- setLoading(true)
1046
- try {
1047
- const db = new Database(taruviClient)
1048
- const response = await db.from("table").execute()
1049
- setData(response.data)
1050
- } catch (err) {
1051
- setError(err.message)
1052
- } finally {
1053
- setLoading(false)
1054
- }
1055
- }
1056
- fetchData()
1057
- }, [])
1058
-
1059
- if (loading) return <div>Loading...</div>
1060
- if (error) return <div>Error: {error}</div>
1061
- return <div>{/* Render data */}</div>
1062
- ```
1063
-
1064
- ### Error Handling
1065
-
1066
- ```typescript
1067
- try {
1068
- const user = new User(taruviClient)
1069
- await user.createUser(data)
1070
- } catch (error) {
1071
- if (error.response?.status === 400) {
1072
- console.error("Validation error:", error.response.data)
1073
- } else if (error.response?.status === 401) {
1074
- console.error("Unauthorized")
1075
- } else {
1076
- console.error("Unknown error:", error.message)
1077
- }
1078
- }
1079
- ```
1080
-
1081
- ### Refresh Data After Mutation
1082
-
1083
- ```typescript
1084
- const [items, setItems] = useState([])
1085
-
1086
- const fetchItems = async () => {
1087
- const db = new Database(taruviClient)
1088
- const response = await db.from("items").execute()
1089
- setItems(response.data || [])
1090
- }
1091
-
1092
- const createItem = async (data) => {
1093
- const db = new Database(taruviClient)
1094
- await db.from("items").create(data).execute()
1095
- await fetchItems() // Refresh list
1096
- }
1097
-
1098
- const deleteItem = async (id) => {
1099
- const db = new Database(taruviClient)
1100
- await db.from("items").delete(id).execute()
1101
- await fetchItems() // Refresh list
1102
- }
1103
-
1104
- const updateItem = async (id, data) => {
1105
- const db = new Database(taruviClient)
1106
- await db.from("items").get(id).update(data).execute()
1107
- await fetchItems() // Refresh list
1108
- }
1109
- ```
1110
-
1111
- ---
1112
-
1113
- ## TypeScript Types
1114
-
1115
- ### Import Types
1116
-
1117
- ```typescript
1118
- import type {
1119
- TaruviConfig,
1120
- AuthTokens,
1121
- UserCreateRequest,
1122
- UserResponse,
1123
- UserDataResponse,
1124
- FunctionRequest,
1125
- FunctionResponse,
1126
- FunctionInvocation,
1127
- DatabaseRequest,
1128
- DatabaseResponse,
1129
- DatabaseFilters,
1130
- StorageRequest,
1131
- StorageUpdateRequest,
1132
- StorageResponse,
1133
- StorageFilters,
1134
- SettingsResponse,
1135
- SecretRequest,
1136
- SecretResponse,
1137
- Principal,
1138
- Resource,
1139
- Resources,
1140
- RoleResponse,
1141
- AnalyticsRequest,
1142
- AnalyticsResponse
1143
- } from '@taruvi/sdk'
1144
- ```
1145
-
1146
- ### Type Usage
1147
-
1148
- ```typescript
1149
- const config: TaruviConfig = {
1150
- apiKey: "key",
1151
- appSlug: "app",
1152
- apiUrl: "https://api.taruvi.cloud",
1153
- deskUrl: "https://desk.taruvi.cloud", // optional
1154
- token: "existing-token" // optional
1155
- }
1156
-
1157
- const userData: UserCreateRequest = {
1158
- username: "john",
1159
- email: "john@example.com",
1160
- password: "pass123",
1161
- confirm_password: "pass123",
1162
- first_name: "John",
1163
- last_name: "Doe",
1164
- is_active: true,
1165
- is_staff: false,
1166
- attributes: ""
1167
- }
1168
-
1169
- // Database filters with operators
1170
- const dbFilters: DatabaseFilters = {
1171
- page: 1,
1172
- pageSize: 20,
1173
- ordering: "-created_at",
1174
- status: "active",
1175
- age__gte: 18,
1176
- name__icontains: "john"
1177
- }
1178
-
1179
- // Storage filters with comprehensive options
1180
- const storageFilters: StorageFilters = {
1181
- page: 1,
1182
- pageSize: 50,
1183
- search: "invoice",
1184
- visibility: "public",
1185
- mimetype_category: "document",
1186
- size__gte: 1024,
1187
- size__lte: 10485760,
1188
- created_at__gte: "2024-01-01",
1189
- ordering: "-created_at",
1190
- created_by_me: true
1191
- }
1192
-
1193
- // Policy types for permission checking
1194
- const principal: Principal = {
1195
- id: "user-123",
1196
- roles: ["admin", "editor"],
1197
- attr: { department: "engineering" }
1198
- }
1199
-
1200
- const resources: Resources = [
1201
- {
1202
- entityType: "crm",
1203
- tableName: "accounts",
1204
- recordId: "acc-456",
1205
- attributes: { owner_id: "user-123" },
1206
- actions: ["read", "update", "delete"]
1207
- }
1208
- ]
1209
- ```
1210
-
1211
- ---
1212
-
1213
- ## Filter Operators Reference
1214
-
1215
- ### Database Filter Operators (Django-style)
1216
-
1217
- The Database service supports Django-style field lookups:
1218
-
1219
- | Operator | Description | Example |
1220
- |----------|-------------|---------|
1221
- | `field` | Exact match | `{ status: "active" }` |
1222
- | `field__gte` | Greater than or equal | `{ age__gte: 18 }` |
1223
- | `field__gt` | Greater than | `{ age__gt: 17 }` |
1224
- | `field__lte` | Less than or equal | `{ age__lte: 65 }` |
1225
- | `field__lt` | Less than | `{ age__lt: 66 }` |
1226
- | `field__icontains` | Case-insensitive contains | `{ name__icontains: "john" }` |
1227
- | `field__contains` | Case-sensitive contains | `{ name__contains: "John" }` |
1228
- | `field__istartswith` | Case-insensitive starts with | `{ email__istartswith: "admin" }` |
1229
- | `field__startswith` | Case-sensitive starts with | `{ code__startswith: "PRE" }` |
1230
- | `field__iendswith` | Case-insensitive ends with | `{ domain__iendswith: ".com" }` |
1231
- | `field__endswith` | Case-sensitive ends with | `{ filename__endswith: ".pdf" }` |
1232
- | `field__in` | Value in list | `{ status__in: ["active", "pending"] }` |
1233
- | `field__isnull` | Is null check | `{ deleted_at__isnull: true }` |
1234
- | `ordering` | Sort results | `{ ordering: "-created_at" }` (- for desc) |
1235
- | `page` | Page number | `{ page: 1 }` |
1236
- | `pageSize` | Items per page | `{ pageSize: 20 }` |
1237
-
1238
- ### Populate (Eager Loading)
1239
-
1240
- Use the `populate()` method to eager load related records:
1241
-
1242
- ```typescript
1243
- // Populate accepts an array of relation field names
1244
- db.from("orders").populate(["customer", "items"]).execute()
1245
-
1246
- // This adds ?populate=customer,items to the query string
1247
- ```
1248
-
1249
- | Parameter | Type | Description |
1250
- |-----------|------|-------------|
1251
- | `populate` | `string[]` | Array of relation field names to eager load |
1252
-
1253
- ### Storage Filter Options
1254
-
1255
- The Storage service supports these specialized filters:
1256
-
1257
- | Category | Filters | Description |
1258
- |----------|---------|-------------|
1259
- | **Size** | `size__gte`, `size__lte`, `size__gt`, `size__lt`, `min_size`, `max_size` | Filter by file size in bytes |
1260
- | **Dates** | `created_at__gte`, `created_at__lte`, `created_after`, `created_before`, `updated_at__gte`, `updated_at__lte` | Filter by dates (ISO 8601 format) |
1261
- | **Search** | `search`, `filename__icontains`, `prefix`, `file`, `file__icontains`, `file__startswith`, `file__istartswith`, `metadata_search` | Search for files |
1262
- | **MIME Type** | `mimetype`, `mimetype__in`, `mimetype_category` | Filter by file type (document, image, video, audio, etc.) |
1263
- | **Visibility** | `visibility` | Filter by public/private visibility |
1264
- | **User** | `created_by_me`, `modified_by_me`, `created_by__username`, `created_by__username__icontains` | Filter by user |
1265
- | **Pagination** | `page`, `pageSize` | Paginate results |
1266
- | **Sorting** | `ordering` | Sort results (e.g., "-created_at") |
1267
-
1268
- ---
1269
-
1270
- ## Quick Reference
1271
-
1272
- | Service | Import | Purpose |
1273
- |---------|--------|---------|
1274
- | `Client` | `import { Client }` | Main SDK client |
1275
- | `Auth` | `import { Auth }` | Authentication |
1276
- | `User` | `import { User }` | User management |
1277
- | `Database` | `import { Database }` | App data CRUD |
1278
- | `Storage` | `import { Storage }` | File management |
1279
- | `Functions` | `import { Functions }` | Serverless functions |
1280
- | `Settings` | `import { Settings }` | Site configuration |
1281
- | `Secrets` | `import { Secrets }` | Sensitive data |
1282
- | `Policy` | `import { Policy }` | Resource permissions |
1283
- | `App` | `import { App }` | App roles & config |
1284
- | `Analytics` | `import { Analytics }` | Analytics queries |
1285
-
1286
- ---
1287
-
1288
- ## Chaining Pattern
1289
-
1290
- All query-building services use method chaining:
1291
-
1292
- ```typescript
1293
- // Database
1294
- const db = new Database(taruviClient)
1295
- await db.from("table").get("id").update(data).execute()
1296
- await db.from("table").filters("status", "eq", "active").execute()
1297
- await db.from("table").page(1).populate(["related_field"]).execute()
1298
- await db.from("table").create({ name: "New" }).execute()
1299
-
1300
- // Storage
1301
- const storage = new Storage(taruviClient)
1302
- await storage.from("bucket").delete(["path/to/file.pdf"]).execute()
1303
- await storage.from("bucket").filter({ search: "query" }).execute()
1304
- await storage.from("bucket").download("path/to/file.pdf").execute()
81
+ ```bash
82
+ npm run build # TypeScript compile
83
+ npm test # Vitest unit tests
1305
84
  ```
1306
85
 
1307
- **Always call `.execute()` at the end to run the query!**
86
+ ## Additional resources
1308
87
 
1309
- ---
1310
-
1311
- ## Environment Variables
1312
-
1313
- ```env
1314
- VITE_TARUVI_API_KEY=your-api-key
1315
- VITE_TARUVI_APP_SLUG=your-app
1316
- VITE_TARUVI_API_URL=https://taruvi-site.taruvi.cloud
1317
- ```
1318
-
1319
- ```typescript
1320
- const client = new Client({
1321
- apiKey: import.meta.env.VITE_TARUVI_API_KEY,
1322
- appSlug: import.meta.env.VITE_TARUVI_APP_SLUG,
1323
- apiUrl: import.meta.env.VITE_TARUVI_API_URL
1324
- })
1325
- ```
88
+ - [SDK design context](SDK_DESIGN_CONTEXT.md) — backend API contract notes (SDK ↔ API mapping)
1326
89
 
1327
- ---
90
+ ## Author
1328
91
 
1329
- **Generated from production code examples • Last updated: 2026-01-12**
92
+ Curran C Doddabele · EOX Vantage