@seaverse/dataservice 1.3.0 → 1.6.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
@@ -22,36 +22,46 @@ npm install @seaverse/dataservice
22
22
  ```typescript
23
23
  import { createClient } from '@seaverse/dataservice';
24
24
 
25
- // Create client (Bearer prefix auto-added, uses default SeaVerse API, appId auto-extracted from URL)
26
- const client = createClient({
27
- token: 'your-jwt-token',
25
+ // Auto-fetch token from parent page (iframe)
26
+ const client = await createClient({});
27
+
28
+ // For development/testing, use debugSetToken
29
+ import { debugSetToken } from '@seaverse/dataservice';
30
+ debugSetToken('your-test-token');
31
+ const client = await createClient({});
32
+
33
+ // Store user preferences (single record)
34
+ const userPrefs = client.userData.collection('user_preferences');
35
+ await userPrefs.insert({
36
+ theme: 'dark',
37
+ language: 'en',
38
+ notifications: true,
28
39
  });
29
40
 
30
- // Access user data table
31
- const orders = client.userData.collection('orders');
41
+ // Update preferences
42
+ await userPrefs.patch(userPrefs.id, { theme: 'light' });
32
43
 
33
- // Insert data
34
- const order = await orders.insert({
44
+ // Store multiple items (e.g., orders) - each needs unique collection name
45
+ const order1 = await client.userData.collection('order_001').insert({
35
46
  order_number: 'ORD-001',
36
47
  status: 'pending',
37
48
  total: 99.99,
38
49
  });
39
50
 
40
- // Query data
41
- const pending = await orders
42
- .select()
43
- .eq('data->>status', 'pending')
44
- .order('created_at', { descending: true })
45
- .execute();
46
-
47
- // Update data
48
- await orders.patch(order.id, { status: 'completed' });
51
+ const order2 = await client.userData.collection('order_002').insert({
52
+ order_number: 'ORD-002',
53
+ status: 'shipped',
54
+ total: 149.99,
55
+ });
49
56
 
50
- // Delete single record
51
- await orders.delete(order.id);
57
+ // Or use batch insert for multiple records
58
+ const orders = await client.userData.batchInsert('order', [
59
+ { order_number: 'ORD-003', status: 'pending', total: 79.99 },
60
+ { order_number: 'ORD-004', status: 'pending', total: 199.99 },
61
+ ]);
52
62
 
53
- // Or delete all records in collection
54
- const deletedCount = await orders.deleteAll();
63
+ // Delete a specific record
64
+ await client.userData.collection('order_001').delete(order1.id);
55
65
  ```
56
66
 
57
67
  ## Core Concepts
@@ -94,22 +104,144 @@ The SDK provides access to different data tables with different permission scope
94
104
 
95
105
  ### Collections
96
106
 
97
- Collections are logical groupings within a table. Each collection stores one record per `(user_id, app_id, collection_name)` combination.
107
+ **Critical Understanding**: A collection name identifies a **single record**, not a container for multiple records.
108
+
109
+ The `(user_id, app_id, collection_name)` combination is a **unique constraint** - meaning each collection name can only store ONE record per user and app.
98
110
 
99
- **Important**: To store multiple records, use unique collection names:
111
+ **To store multiple records, you MUST use unique collection names**:
100
112
 
101
113
  ```typescript
102
- // ✓ Correct: Unique collection names
103
- await client.userData.collection('order_1').insert(order1);
104
- await client.userData.collection('order_2').insert(order2);
114
+ // ✓ CORRECT: Each record gets a unique collection name
115
+ await client.userData.collection('order_001').insert(order1);
116
+ await client.userData.collection('order_002').insert(order2);
117
+ await client.userData.collection('order_003').insert(order3);
105
118
 
106
- // ✓ Or use batch insert helper
107
- await client.userData.batchInsert('order', [order1, order2]);
108
- // Creates: order_0, order_1, order_2, ...
119
+ // ✓ RECOMMENDED: Use batch insert (auto-generates unique names)
120
+ const insertedOrders = await client.userData.batchInsert('order', [order1, order2, order3]);
121
+ // Creates collections: order_<timestamp>_0, order_<timestamp>_1, order_<timestamp>_2
109
122
 
110
- // ✗ Wrong: Same collection name
111
- await client.userData.collection('order').insert(order1);
112
- await client.userData.collection('order').insert(order2); // Error: 409 Conflict
123
+ // ✗ WRONG: Reusing the same collection name
124
+ const orders = client.userData.collection('orders');
125
+ await orders.insert(order1); // Success
126
+ await orders.insert(order2); // ✗ ERROR: 409 Conflict (collection 'orders' already exists)
127
+ ```
128
+
129
+ **Why this design?**
130
+ - Each collection name acts as a **unique key** for a single record
131
+ - Think of it like a key-value store: `collection_name` → single record
132
+ - For multiple records, use patterns like: `order_${orderId}`, `msg_${conversationId}_${msgId}`
133
+
134
+ ## ⚠️ Common Pitfalls
135
+
136
+ ### Pitfall #1: Treating Collections as Containers
137
+
138
+ ```typescript
139
+ // ❌ WRONG: This looks like it should work, but it doesn't
140
+ const orders = client.userData.collection('orders');
141
+ await orders.insert({ order_number: 'ORD-001', total: 99.99 }); // ✓ Works
142
+ await orders.insert({ order_number: 'ORD-002', total: 149.99 }); // ✗ ERROR: 409 Conflict!
143
+
144
+ // ✓ CORRECT: Each record needs a unique collection name
145
+ await client.userData.collection('order_001').insert({ order_number: 'ORD-001', total: 99.99 });
146
+ await client.userData.collection('order_002').insert({ order_number: 'ORD-002', total: 149.99 });
147
+ ```
148
+
149
+ **Why?** The `(user_id, app_id, collection_name)` combination is a unique constraint. Once you insert into `'orders'`, that collection name is "taken" for that user and app.
150
+
151
+ ### Pitfall #2: Expecting Query Methods to Return Multiple Records
152
+
153
+ ```typescript
154
+ // ❌ MISLEADING: This looks like it queries multiple orders
155
+ const orders = client.userData.collection('orders');
156
+ const pending = await orders.select().eq('data->>status', 'pending').execute();
157
+ // Returns: [] or [single order] - NOT multiple orders!
158
+
159
+ // ✓ CORRECT: To work with multiple records, track them separately
160
+ const orderIds = ['order_001', 'order_002', 'order_003'];
161
+ const allOrders = await Promise.all(
162
+ orderIds.map(id => client.userData.collection(id).get(recordId))
163
+ );
164
+ ```
165
+
166
+ **Why?** A collection name identifies a single record, so queries on that collection can only return 0 or 1 record.
167
+
168
+ ### Pitfall #3: Using Plural Names for Collections
169
+
170
+ ```typescript
171
+ // ❌ MISLEADING: Plural name suggests multiple items
172
+ const orders = client.userData.collection('orders');
173
+ const users = client.userData.collection('users');
174
+
175
+ // ✓ BETTER: Use singular or ID-based names
176
+ const order = client.userData.collection('order_12345');
177
+ const userProfile = client.userData.collection('user_profile');
178
+ const preference = client.userData.collection('user_preferences');
179
+ ```
180
+
181
+ **Why?** Plural names create false expectations. Use singular names or include IDs to make it clear each collection is one record.
182
+
183
+ ## 💡 Best Practices
184
+
185
+ ### Pattern 1: Single Record per Concept
186
+
187
+ Use this for user preferences, settings, profiles - things where you only need one record:
188
+
189
+ ```typescript
190
+ // User preferences (one per user)
191
+ const prefs = await client.userData.collection('preferences').insert({
192
+ theme: 'dark',
193
+ language: 'en',
194
+ });
195
+
196
+ // User profile (one per user)
197
+ const profile = await client.userData.collection('profile').insert({
198
+ name: 'John Doe',
199
+ avatar: 'https://...',
200
+ });
201
+ ```
202
+
203
+ ### Pattern 2: ID-Based Collection Names
204
+
205
+ Use this for multiple items of the same type:
206
+
207
+ ```typescript
208
+ // Multiple orders - each with unique collection name
209
+ const order1 = await client.userData.collection(`order_${orderId1}`).insert(orderData1);
210
+ const order2 = await client.userData.collection(`order_${orderId2}`).insert(orderData2);
211
+
212
+ // Multiple conversations
213
+ const conv1 = await client.userData.collection(`conv_${convId1}`).insert(convData1);
214
+ const conv2 = await client.userData.collection(`conv_${convId2}`).insert(convData2);
215
+ ```
216
+
217
+ ### Pattern 3: Batch Insert for Multiple Records
218
+
219
+ Use this when creating multiple records at once:
220
+
221
+ ```typescript
222
+ // Create multiple orders in one call
223
+ const orders = await client.userData.batchInsert('order', [
224
+ { order_number: 'ORD-001', total: 99.99 },
225
+ { order_number: 'ORD-002', total: 149.99 },
226
+ { order_number: 'ORD-003', total: 199.99 },
227
+ ]);
228
+
229
+ // Returns array of DataRecord with auto-generated collection names
230
+ // order_<timestamp>_0, order_<timestamp>_1, order_<timestamp>_2
231
+ ```
232
+
233
+ ### Pattern 4: Hierarchical Collection Names
234
+
235
+ Use this for nested data structures:
236
+
237
+ ```typescript
238
+ // Messages within a conversation
239
+ await client.userData.collection(`conv_${convId}_msg_1`).insert(message1);
240
+ await client.userData.collection(`conv_${convId}_msg_2`).insert(message2);
241
+
242
+ // Tasks within a project
243
+ await client.userData.collection(`project_${projId}_task_1`).insert(task1);
244
+ await client.userData.collection(`project_${projId}_task_2`).insert(task2);
113
245
  ```
114
246
 
115
247
  ### Data Records
@@ -136,33 +268,77 @@ interface DataRecord<T> {
136
268
  ```typescript
137
269
  import { createClient } from '@seaverse/dataservice';
138
270
 
139
- const client = createClient({
140
- token: string; // JWT token (Bearer prefix optional, auto-added if missing)
271
+ const client = await createClient({
141
272
  url?: string; // PostgREST API URL (default: https://dataservice-api.seaverse.ai)
142
273
  options?: {
143
274
  timeout?: number; // Request timeout in ms (default: 30000)
275
+ tokenFetchTimeout?: number; // Token fetch timeout in ms (default: 5000)
144
276
  headers?: Record<string, string>; // Additional headers
145
277
  };
146
278
  });
147
279
  ```
148
280
 
149
- **Examples:**
281
+ **Token Authentication Priority:**
282
+
283
+ The SDK uses the following priority order for obtaining authentication tokens:
284
+
285
+ 1. **Debug Token (Highest Priority)** - Set via `debugSetToken()`
286
+ 2. **Parent Page Token** - Auto-fetched via PostMessage when in iframe
287
+ 3. **No Token** - Client created without authentication (API calls will fail with 401)
288
+
289
+ **Production Usage (Auto-fetch from parent):**
290
+
291
+ The SDK automatically fetches the authentication token from the parent page via PostMessage when running in an iframe:
150
292
 
151
293
  ```typescript
152
- // Use default SeaVerse API (Bearer prefix auto-added)
153
- const client = createClient({
154
- token: 'your-jwt-token',
155
- });
294
+ // No token needed - auto-fetches from parent
295
+ const client = await createClient({});
156
296
 
157
- // Or with explicit Bearer prefix (both work)
158
- const client = createClient({
159
- token: 'Bearer your-jwt-token',
160
- });
297
+ // Parent page should respond to PostMessage:
298
+ // Send: { type: 'seaverse:get_token' }
299
+ // Receive: { type: 'seaverse:token', payload: { accessToken: string, expiresIn: number } }
300
+ // Error: { type: 'seaverse:error', error: string }
301
+ ```
302
+
303
+ **Development/Testing (Debug Token):**
304
+
305
+ For development and testing, use `debugSetToken` to bypass the parent page token fetch:
306
+
307
+ ```typescript
308
+ import { debugSetToken, createClient } from '@seaverse/dataservice';
309
+
310
+ // Set debug token BEFORE creating client (highest priority)
311
+ debugSetToken('your-test-token');
312
+
313
+ // Client will use debug token directly, skipping parent page fetch
314
+ const client = await createClient({});
315
+ ```
316
+
317
+ **Graceful Degradation:**
318
+
319
+ If token fetching fails (e.g., not in an iframe, parent doesn't respond), the SDK will create a client without a token. API calls that require authentication will return 401 errors:
320
+
321
+ ```typescript
322
+ // Client creation never throws, even if token fetch fails
323
+ const client = await createClient({});
324
+
325
+ try {
326
+ // API calls may fail with authentication errors if no token
327
+ const data = await client.userData.collection('test').insert({ foo: 'bar' });
328
+ } catch (error) {
329
+ if (error instanceof DataServiceError && error.statusCode === 401) {
330
+ console.log('Authentication required - no token available');
331
+ // Handle authentication error (e.g., show login prompt)
332
+ }
333
+ }
334
+ ```
335
+
336
+ **Custom Endpoint:**
161
337
 
338
+ ```typescript
162
339
  // Use custom API endpoint
163
- const client = createClient({
340
+ const client = await createClient({
164
341
  url: 'https://your-custom-api.example.com',
165
- token: 'your-jwt-token',
166
342
  });
167
343
  ```
168
344
 
@@ -221,17 +397,8 @@ collection.patch(id: string, partial: Partial<T>): Promise<DataRecord<T>>
221
397
  #### Delete
222
398
 
223
399
  ```typescript
224
- // Hard delete single record (permanent)
400
+ // Delete a record by ID (permanent deletion)
225
401
  collection.delete(id: string): Promise<void>
226
-
227
- // Hard delete all records in collection (returns count of deleted records)
228
- collection.deleteAll(): Promise<number>
229
-
230
- // Soft delete (sets deleted_at timestamp)
231
- collection.softDelete(id: string): Promise<boolean>
232
-
233
- // Restore soft-deleted record
234
- collection.restore(id: string): Promise<boolean>
235
402
  ```
236
403
 
237
404
  ### Query Builder
@@ -309,15 +476,15 @@ type Order = {
309
476
  notes?: string;
310
477
  };
311
478
 
312
- const client = createClient({
313
- token: process.env.JWT_TOKEN!,
314
- });
315
-
316
- const orders = client.userData.collection<Order>('orders');
479
+ // For development/testing with Node.js
480
+ import { debugSetToken, createClient } from '@seaverse/dataservice';
481
+ debugSetToken(process.env.JWT_TOKEN!);
482
+ const client = await createClient({});
317
483
 
318
- // Create order
319
- const order = await orders.insert({
320
- order_number: `ORD-${Date.now()}`,
484
+ // Create multiple orders - each with unique collection name
485
+ const orderNumber1 = `ORD-${Date.now()}`;
486
+ const order1 = await client.userData.collection<Order>(`order_${orderNumber1}`).insert({
487
+ order_number: orderNumber1,
321
488
  customer_email: 'customer@example.com',
322
489
  items: [
323
490
  { product_id: 'PROD-001', quantity: 2, price: 29.99 },
@@ -327,33 +494,36 @@ const order = await orders.insert({
327
494
  total: 109.97,
328
495
  });
329
496
 
330
- console.log('Order created:', order.id);
331
-
332
- // Query pending orders over $50
333
- const pendingOrders = await orders
334
- .select()
335
- .eq('data->>status', 'pending')
336
- .gt('data->total', '50')
337
- .order('created_at', { descending: true })
338
- .limit(10)
339
- .execute();
340
-
341
- console.log(`Found ${pendingOrders.length} pending orders`);
497
+ console.log('Order created:', order1.id, 'Collection:', `order_${orderNumber1}`);
342
498
 
343
- // Update order status
344
- await orders.patch(order.id, { status: 'shipped' });
345
-
346
- // Search by customer email
347
- const customerOrders = await orders.search({
499
+ // Create another order
500
+ const orderNumber2 = `ORD-${Date.now() + 1}`;
501
+ const order2 = await client.userData.collection<Order>(`order_${orderNumber2}`).insert({
502
+ order_number: orderNumber2,
348
503
  customer_email: 'customer@example.com',
504
+ items: [
505
+ { product_id: 'PROD-003', quantity: 1, price: 199.99 },
506
+ ],
507
+ status: 'pending',
508
+ total: 199.99,
349
509
  });
350
510
 
511
+ // Update order status (access by collection name)
512
+ await client.userData.collection(`order_${orderNumber1}`).patch(order1.id, {
513
+ status: 'shipped'
514
+ });
515
+
516
+ // Get specific order
517
+ const retrieved = await client.userData.collection(`order_${orderNumber1}`).get(order1.id);
518
+ console.log('Order status:', retrieved?.data.status);
519
+
351
520
  // Delete a specific order
352
- await orders.delete(order.id);
521
+ await client.userData.collection(`order_${orderNumber1}`).delete(order1.id);
353
522
 
354
- // Delete all orders (useful for cleanup)
355
- const deletedCount = await orders.deleteAll();
356
- console.log(`Deleted ${deletedCount} orders`);
523
+ // Note: To query across multiple orders, you would need to:
524
+ // 1. Store order metadata in a separate tracking collection
525
+ // 2. Or use a naming convention and iterate through known order IDs
526
+ // 3. Or use the batchInsert pattern and track the generated collection names
357
527
  ```
358
528
 
359
529
  ### Chat Conversations
@@ -375,10 +545,9 @@ type Conversation = {
375
545
  };
376
546
  };
377
547
 
378
- const conversations = client.userData.collection<Conversation>('chats');
379
-
380
- // Create conversation
381
- const conv = await conversations.insert({
548
+ // Create a new conversation with unique ID
549
+ const conversationId = `conv_${Date.now()}`;
550
+ const conv = await client.userData.collection<Conversation>(conversationId).insert({
382
551
  title: 'Project Planning Discussion',
383
552
  model: 'claude-3-opus',
384
553
  messages: [
@@ -390,10 +559,12 @@ const conv = await conversations.insert({
390
559
  ],
391
560
  });
392
561
 
393
- // Add assistant response
394
- const current = await conversations.get(conv.id);
562
+ console.log('Conversation created:', conversationId);
563
+
564
+ // Add assistant response to existing conversation
565
+ const current = await client.userData.collection<Conversation>(conversationId).get(conv.id);
395
566
  if (current) {
396
- await conversations.patch(conv.id, {
567
+ await client.userData.collection<Conversation>(conversationId).patch(conv.id, {
397
568
  messages: [
398
569
  ...current.data.messages,
399
570
  {
@@ -405,12 +576,23 @@ if (current) {
405
576
  });
406
577
  }
407
578
 
408
- // List recent conversations
409
- const recent = await conversations
410
- .select()
411
- .order('updated_at', { descending: true })
412
- .limit(20)
413
- .execute();
579
+ // Store individual messages separately (alternative pattern)
580
+ // Each message gets its own collection
581
+ const msgId1 = await client.userData.collection(`${conversationId}_msg_1`).insert({
582
+ role: 'user',
583
+ content: 'Help me plan a new feature',
584
+ timestamp: new Date().toISOString(),
585
+ });
586
+
587
+ const msgId2 = await client.userData.collection(`${conversationId}_msg_2`).insert({
588
+ role: 'assistant',
589
+ content: 'I can help with that!',
590
+ timestamp: new Date().toISOString(),
591
+ });
592
+
593
+ // Get a specific conversation
594
+ const retrieved = await client.userData.collection(conversationId).get(conv.id);
595
+ console.log('Conversation title:', retrieved?.data.title);
414
596
  ```
415
597
 
416
598
  ### User Preferences
package/dist/index.d.mts CHANGED
@@ -12,15 +12,13 @@
12
12
  * @example
13
13
  * ```typescript
14
14
  * const config: ClientConfig = {
15
- * token: 'Bearer your-jwt-token-here',
15
+ * url: 'https://dataservice-api.seaverse.ai',
16
16
  * };
17
17
  * ```
18
18
  */
19
19
  interface ClientConfig {
20
20
  /** PostgREST API base URL (default: https://dataservice-api.seaverse.ai) */
21
21
  url?: string;
22
- /** JWT token containing user_id in payload.user_id */
23
- token: string;
24
22
  /** Optional configuration */
25
23
  options?: {
26
24
  /** Custom fetch implementation (useful for Node.js < 18) */
@@ -29,6 +27,8 @@ interface ClientConfig {
29
27
  headers?: Record<string, string>;
30
28
  /** Request timeout in milliseconds (default: 30000) */
31
29
  timeout?: number;
30
+ /** Timeout for fetching token from parent (milliseconds, default: 5000) */
31
+ tokenFetchTimeout?: number;
32
32
  };
33
33
  }
34
34
  /**
@@ -193,14 +193,8 @@ interface Collection<T = any> {
193
193
  update(id: string, data: T): Promise<DataRecord<T>>;
194
194
  /** Patch a record by ID (merges with existing data) */
195
195
  patch(id: string, partial: Partial<T>): Promise<DataRecord<T>>;
196
- /** Hard delete a record by ID */
196
+ /** Delete a record by ID (permanent) */
197
197
  delete(id: string): Promise<void>;
198
- /** Hard delete all records in this collection */
199
- deleteAll(): Promise<number>;
200
- /** Soft delete a record by ID (sets deleted_at) */
201
- softDelete(id: string): Promise<boolean>;
202
- /** Restore a soft-deleted record */
203
- restore(id: string): Promise<boolean>;
204
198
  /** Search records by JSONB contains */
205
199
  search(criteria: Partial<T>): Promise<DataRecord<T>[]>;
206
200
  /** Count records in collection */
@@ -246,39 +240,53 @@ interface DataServiceClient {
246
240
  * 4. Secure: RLS enforced, token-based auth
247
241
  */
248
242
 
243
+ /**
244
+ * Set debug token for testing/debugging
245
+ * This allows you to bypass the token fetch logic during development
246
+ *
247
+ * @param token The token to use for debugging
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * import { debugSetToken, createClient } from '@seaverse/dataservice';
252
+ *
253
+ * // Set debug token before creating client
254
+ * debugSetToken('your-test-token');
255
+ *
256
+ * // Client will use debug token instead of fetching
257
+ * const client = await createClient({});
258
+ * ```
259
+ */
260
+ declare function debugSetToken(token: string): void;
249
261
  /**
250
262
  * Create a new Data Service client
251
263
  *
252
264
  * AI-friendly: Single entry point with clear configuration
253
265
  *
254
- * @param config - Client configuration with JWT token (URL defaults to https://dataservice-api.seaverse.ai)
266
+ * @param config - Client configuration
255
267
  * @returns DataServiceClient instance
256
268
  *
257
269
  * @example
258
270
  * ```typescript
259
- * // Use default SeaVerse API endpoint (Bearer prefix auto-added)
260
- * const client = createClient({
261
- * token: 'your-jwt-token-here',
262
- * });
263
- *
264
- * // Or with explicit Bearer prefix (both work)
265
- * const client = createClient({
266
- * token: 'Bearer your-jwt-token-here',
267
- * });
271
+ * // Auto-fetch token from parent page (iframe)
272
+ * const client = await createClient({});
268
273
  *
269
- * // Or specify custom endpoint
270
- * const client = createClient({
274
+ * // Custom endpoint
275
+ * const client = await createClient({
271
276
  * url: 'https://your-postgrest-api.example.com',
272
- * token: 'your-jwt-token-here',
273
277
  * });
274
278
  *
279
+ * // For development/testing, use debugSetToken
280
+ * import { debugSetToken, createClient } from '@seaverse/dataservice';
281
+ * debugSetToken('your-test-token');
282
+ * const client = await createClient({});
283
+ *
275
284
  * // appId is automatically extracted from current URL
276
- * // Direct access to collections - clean and simple
277
285
  * const order = await client.userData.collection('orders').insert({ ... });
278
286
  * const orders = await client.userData.collection('orders').select().execute();
279
287
  * ```
280
288
  */
281
- declare function createClient(config: ClientConfig): DataServiceClient;
289
+ declare function createClient(config?: ClientConfig): Promise<DataServiceClient>;
282
290
 
283
291
  /**
284
292
  * SeaVerse Data Service SDK
@@ -287,18 +295,20 @@ declare function createClient(config: ClientConfig): DataServiceClient;
287
295
  *
288
296
  * @packageDocumentation
289
297
  *
290
- * @example Basic Usage
298
+ * @example Basic Usage with Auto Token Fetch
291
299
  * ```typescript
292
- * import { createClient } from '@seaverse/dataservice';
300
+ * import { createClient, debugSetToken } from '@seaverse/dataservice';
293
301
  *
294
- * // Create client (Bearer prefix auto-added, uses default SeaVerse API)
295
- * const client = createClient({
296
- * token: 'your-jwt-token',
297
- * });
302
+ * // Auto-fetch token from parent page (iframe)
303
+ * const client = await createClient({});
304
+ *
305
+ * // For development/testing, use debugSetToken
306
+ * debugSetToken('your-test-token');
307
+ * const client = await createClient({});
298
308
  *
299
309
  * // appId is automatically extracted from current URL
300
310
  * // Insert data
301
- * const order = await client.userData.collection('orders').insert({
311
+ * const order = await client.userData.collection('order_001').insert({
302
312
  * order_number: 'ORD-123',
303
313
  * status: 'pending',
304
314
  * total: 99.99,
@@ -306,7 +316,7 @@ declare function createClient(config: ClientConfig): DataServiceClient;
306
316
  *
307
317
  * // Query data
308
318
  * const orders = await client.userData
309
- * .collection('orders')
319
+ * .collection('order_001')
310
320
  * .select()
311
321
  * .eq('data->>status', 'pending')
312
322
  * .order('created_at', { descending: true })
@@ -317,4 +327,4 @@ declare function createClient(config: ClientConfig): DataServiceClient;
317
327
 
318
328
  declare const VERSION = "1.0.0";
319
329
 
320
- export { type APIError, type ClientConfig, type Collection, type DataRecord, type DataServiceClient, DataServiceError, type DataTable, type OrderOptions, type QueryBuilder, type QueryFilter, type UserDataStats, VERSION, createClient };
330
+ export { type APIError, type ClientConfig, type Collection, type DataRecord, type DataServiceClient, DataServiceError, type DataTable, type OrderOptions, type QueryBuilder, type QueryFilter, type UserDataStats, VERSION, createClient, debugSetToken };
package/dist/index.d.ts CHANGED
@@ -12,15 +12,13 @@
12
12
  * @example
13
13
  * ```typescript
14
14
  * const config: ClientConfig = {
15
- * token: 'Bearer your-jwt-token-here',
15
+ * url: 'https://dataservice-api.seaverse.ai',
16
16
  * };
17
17
  * ```
18
18
  */
19
19
  interface ClientConfig {
20
20
  /** PostgREST API base URL (default: https://dataservice-api.seaverse.ai) */
21
21
  url?: string;
22
- /** JWT token containing user_id in payload.user_id */
23
- token: string;
24
22
  /** Optional configuration */
25
23
  options?: {
26
24
  /** Custom fetch implementation (useful for Node.js < 18) */
@@ -29,6 +27,8 @@ interface ClientConfig {
29
27
  headers?: Record<string, string>;
30
28
  /** Request timeout in milliseconds (default: 30000) */
31
29
  timeout?: number;
30
+ /** Timeout for fetching token from parent (milliseconds, default: 5000) */
31
+ tokenFetchTimeout?: number;
32
32
  };
33
33
  }
34
34
  /**
@@ -193,14 +193,8 @@ interface Collection<T = any> {
193
193
  update(id: string, data: T): Promise<DataRecord<T>>;
194
194
  /** Patch a record by ID (merges with existing data) */
195
195
  patch(id: string, partial: Partial<T>): Promise<DataRecord<T>>;
196
- /** Hard delete a record by ID */
196
+ /** Delete a record by ID (permanent) */
197
197
  delete(id: string): Promise<void>;
198
- /** Hard delete all records in this collection */
199
- deleteAll(): Promise<number>;
200
- /** Soft delete a record by ID (sets deleted_at) */
201
- softDelete(id: string): Promise<boolean>;
202
- /** Restore a soft-deleted record */
203
- restore(id: string): Promise<boolean>;
204
198
  /** Search records by JSONB contains */
205
199
  search(criteria: Partial<T>): Promise<DataRecord<T>[]>;
206
200
  /** Count records in collection */
@@ -246,39 +240,53 @@ interface DataServiceClient {
246
240
  * 4. Secure: RLS enforced, token-based auth
247
241
  */
248
242
 
243
+ /**
244
+ * Set debug token for testing/debugging
245
+ * This allows you to bypass the token fetch logic during development
246
+ *
247
+ * @param token The token to use for debugging
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * import { debugSetToken, createClient } from '@seaverse/dataservice';
252
+ *
253
+ * // Set debug token before creating client
254
+ * debugSetToken('your-test-token');
255
+ *
256
+ * // Client will use debug token instead of fetching
257
+ * const client = await createClient({});
258
+ * ```
259
+ */
260
+ declare function debugSetToken(token: string): void;
249
261
  /**
250
262
  * Create a new Data Service client
251
263
  *
252
264
  * AI-friendly: Single entry point with clear configuration
253
265
  *
254
- * @param config - Client configuration with JWT token (URL defaults to https://dataservice-api.seaverse.ai)
266
+ * @param config - Client configuration
255
267
  * @returns DataServiceClient instance
256
268
  *
257
269
  * @example
258
270
  * ```typescript
259
- * // Use default SeaVerse API endpoint (Bearer prefix auto-added)
260
- * const client = createClient({
261
- * token: 'your-jwt-token-here',
262
- * });
263
- *
264
- * // Or with explicit Bearer prefix (both work)
265
- * const client = createClient({
266
- * token: 'Bearer your-jwt-token-here',
267
- * });
271
+ * // Auto-fetch token from parent page (iframe)
272
+ * const client = await createClient({});
268
273
  *
269
- * // Or specify custom endpoint
270
- * const client = createClient({
274
+ * // Custom endpoint
275
+ * const client = await createClient({
271
276
  * url: 'https://your-postgrest-api.example.com',
272
- * token: 'your-jwt-token-here',
273
277
  * });
274
278
  *
279
+ * // For development/testing, use debugSetToken
280
+ * import { debugSetToken, createClient } from '@seaverse/dataservice';
281
+ * debugSetToken('your-test-token');
282
+ * const client = await createClient({});
283
+ *
275
284
  * // appId is automatically extracted from current URL
276
- * // Direct access to collections - clean and simple
277
285
  * const order = await client.userData.collection('orders').insert({ ... });
278
286
  * const orders = await client.userData.collection('orders').select().execute();
279
287
  * ```
280
288
  */
281
- declare function createClient(config: ClientConfig): DataServiceClient;
289
+ declare function createClient(config?: ClientConfig): Promise<DataServiceClient>;
282
290
 
283
291
  /**
284
292
  * SeaVerse Data Service SDK
@@ -287,18 +295,20 @@ declare function createClient(config: ClientConfig): DataServiceClient;
287
295
  *
288
296
  * @packageDocumentation
289
297
  *
290
- * @example Basic Usage
298
+ * @example Basic Usage with Auto Token Fetch
291
299
  * ```typescript
292
- * import { createClient } from '@seaverse/dataservice';
300
+ * import { createClient, debugSetToken } from '@seaverse/dataservice';
293
301
  *
294
- * // Create client (Bearer prefix auto-added, uses default SeaVerse API)
295
- * const client = createClient({
296
- * token: 'your-jwt-token',
297
- * });
302
+ * // Auto-fetch token from parent page (iframe)
303
+ * const client = await createClient({});
304
+ *
305
+ * // For development/testing, use debugSetToken
306
+ * debugSetToken('your-test-token');
307
+ * const client = await createClient({});
298
308
  *
299
309
  * // appId is automatically extracted from current URL
300
310
  * // Insert data
301
- * const order = await client.userData.collection('orders').insert({
311
+ * const order = await client.userData.collection('order_001').insert({
302
312
  * order_number: 'ORD-123',
303
313
  * status: 'pending',
304
314
  * total: 99.99,
@@ -306,7 +316,7 @@ declare function createClient(config: ClientConfig): DataServiceClient;
306
316
  *
307
317
  * // Query data
308
318
  * const orders = await client.userData
309
- * .collection('orders')
319
+ * .collection('order_001')
310
320
  * .select()
311
321
  * .eq('data->>status', 'pending')
312
322
  * .order('created_at', { descending: true })
@@ -317,4 +327,4 @@ declare function createClient(config: ClientConfig): DataServiceClient;
317
327
 
318
328
  declare const VERSION = "1.0.0";
319
329
 
320
- export { type APIError, type ClientConfig, type Collection, type DataRecord, type DataServiceClient, DataServiceError, type DataTable, type OrderOptions, type QueryBuilder, type QueryFilter, type UserDataStats, VERSION, createClient };
330
+ export { type APIError, type ClientConfig, type Collection, type DataRecord, type DataServiceClient, DataServiceError, type DataTable, type OrderOptions, type QueryBuilder, type QueryFilter, type UserDataStats, VERSION, createClient, debugSetToken };
package/dist/index.js CHANGED
@@ -22,7 +22,8 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  DataServiceError: () => DataServiceError,
24
24
  VERSION: () => VERSION,
25
- createClient: () => createClient
25
+ createClient: () => createClient,
26
+ debugSetToken: () => debugSetToken
26
27
  });
27
28
  module.exports = __toCommonJS(src_exports);
28
29
 
@@ -39,6 +40,54 @@ var DataServiceError = class extends Error {
39
40
  };
40
41
 
41
42
  // src/client.ts
43
+ var debugToken = null;
44
+ function debugSetToken(token) {
45
+ debugToken = token;
46
+ }
47
+ function isInIframe() {
48
+ try {
49
+ return typeof globalThis !== "undefined" && "window" in globalThis && globalThis.window.self !== globalThis.window.top;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+ async function getTokenFromParent(timeout = 5e3) {
55
+ if (!isInIframe()) {
56
+ return null;
57
+ }
58
+ return new Promise((resolve) => {
59
+ const messageHandler = (event) => {
60
+ if (event.data && event.data.type === "seaverse:token") {
61
+ cleanup();
62
+ const token = event.data.payload?.accessToken;
63
+ resolve(token || null);
64
+ } else if (event.data && event.data.type === "seaverse:error") {
65
+ cleanup();
66
+ console.warn("[SeaVerse DataService SDK] Error getting token from parent:", event.data.error);
67
+ resolve(null);
68
+ }
69
+ };
70
+ const timeoutId = setTimeout(() => {
71
+ cleanup();
72
+ resolve(null);
73
+ }, timeout);
74
+ const cleanup = () => {
75
+ clearTimeout(timeoutId);
76
+ globalThis.window.removeEventListener("message", messageHandler);
77
+ };
78
+ globalThis.window.addEventListener("message", messageHandler);
79
+ try {
80
+ globalThis.window.parent.postMessage(
81
+ { type: "seaverse:get_token" },
82
+ "*"
83
+ // Allow any origin, supports cross-domain scenarios
84
+ );
85
+ } catch (e) {
86
+ cleanup();
87
+ resolve(null);
88
+ }
89
+ });
90
+ }
42
91
  function extractAppId() {
43
92
  if (typeof globalThis !== "undefined" && "location" in globalThis) {
44
93
  const location = globalThis.location;
@@ -56,17 +105,27 @@ var HTTPClient = class {
56
105
  headers;
57
106
  fetchFn;
58
107
  timeout;
59
- constructor(config) {
108
+ tokenPromise = null;
109
+ constructor(config, token) {
60
110
  this.baseUrl = (config.url || "https://dataservice-api.seaverse.ai").replace(/\/$/, "");
61
111
  this.fetchFn = config.options?.fetch || globalThis.fetch;
62
112
  this.timeout = config.options?.timeout || 3e4;
63
- const token = config.token.startsWith("Bearer ") ? config.token : `Bearer ${config.token}`;
64
113
  this.headers = {
65
- "Authorization": token,
66
114
  "Content-Type": "application/json",
67
115
  "Prefer": "return=representation",
68
116
  ...config.options?.headers
69
117
  };
118
+ if (token) {
119
+ const authToken = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
120
+ this.headers["Authorization"] = authToken;
121
+ }
122
+ }
123
+ /**
124
+ * Set authentication token (called after async token fetch)
125
+ */
126
+ setToken(token) {
127
+ const authToken = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
128
+ this.headers["Authorization"] = authToken;
70
129
  }
71
130
  /**
72
131
  * Make HTTP request with timeout and error handling
@@ -286,30 +345,6 @@ var CollectionImpl = class {
286
345
  `/user_data?id=eq.${id}&app_id=eq.${this.appId}&collection_name=eq.${this.collectionName}`
287
346
  );
288
347
  }
289
- async deleteAll() {
290
- const records = await this.client.get(
291
- `/user_data?app_id=eq.${this.appId}&collection_name=eq.${this.collectionName}`
292
- );
293
- const count = records.length;
294
- if (count > 0) {
295
- await this.client.delete(
296
- `/user_data?app_id=eq.${this.appId}&collection_name=eq.${this.collectionName}`
297
- );
298
- }
299
- return count;
300
- }
301
- async softDelete(id) {
302
- const response = await this.client.post("/rpc/soft_delete_user_data", {
303
- p_id: id
304
- });
305
- return response;
306
- }
307
- async restore(id) {
308
- const response = await this.client.post("/rpc/restore_user_data", {
309
- p_id: id
310
- });
311
- return response;
312
- }
313
348
  async search(criteria) {
314
349
  return this.client.get(this.tablePath, {
315
350
  app_id: `eq.${this.appId}`,
@@ -357,9 +392,9 @@ var DataServiceClientImpl = class {
357
392
  client;
358
393
  userData;
359
394
  appId;
360
- constructor(config) {
361
- this.client = new HTTPClient(config);
362
- this.appId = extractAppId();
395
+ constructor(client, appId) {
396
+ this.client = client;
397
+ this.appId = appId;
363
398
  this.userData = new DataTableImpl(this.client, "/user_data", this.appId);
364
399
  }
365
400
  async getStats() {
@@ -370,8 +405,22 @@ var DataServiceClientImpl = class {
370
405
  return this.client.post("/rpc/health");
371
406
  }
372
407
  };
373
- function createClient(config) {
374
- return new DataServiceClientImpl(config);
408
+ async function createClient(config = {}) {
409
+ let token = null;
410
+ if (debugToken) {
411
+ token = debugToken;
412
+ } else {
413
+ try {
414
+ const timeout = config.options?.tokenFetchTimeout || 5e3;
415
+ token = await getTokenFromParent(timeout);
416
+ } catch (error) {
417
+ console.warn("[SeaVerse DataService SDK] Failed to fetch token:", error);
418
+ token = null;
419
+ }
420
+ }
421
+ const httpClient = new HTTPClient(config, token || void 0);
422
+ const appId = extractAppId();
423
+ return new DataServiceClientImpl(httpClient, appId);
375
424
  }
376
425
 
377
426
  // src/index.ts
@@ -380,5 +429,6 @@ var VERSION = "1.0.0";
380
429
  0 && (module.exports = {
381
430
  DataServiceError,
382
431
  VERSION,
383
- createClient
432
+ createClient,
433
+ debugSetToken
384
434
  });
package/dist/index.mjs CHANGED
@@ -11,6 +11,54 @@ var DataServiceError = class extends Error {
11
11
  };
12
12
 
13
13
  // src/client.ts
14
+ var debugToken = null;
15
+ function debugSetToken(token) {
16
+ debugToken = token;
17
+ }
18
+ function isInIframe() {
19
+ try {
20
+ return typeof globalThis !== "undefined" && "window" in globalThis && globalThis.window.self !== globalThis.window.top;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+ async function getTokenFromParent(timeout = 5e3) {
26
+ if (!isInIframe()) {
27
+ return null;
28
+ }
29
+ return new Promise((resolve) => {
30
+ const messageHandler = (event) => {
31
+ if (event.data && event.data.type === "seaverse:token") {
32
+ cleanup();
33
+ const token = event.data.payload?.accessToken;
34
+ resolve(token || null);
35
+ } else if (event.data && event.data.type === "seaverse:error") {
36
+ cleanup();
37
+ console.warn("[SeaVerse DataService SDK] Error getting token from parent:", event.data.error);
38
+ resolve(null);
39
+ }
40
+ };
41
+ const timeoutId = setTimeout(() => {
42
+ cleanup();
43
+ resolve(null);
44
+ }, timeout);
45
+ const cleanup = () => {
46
+ clearTimeout(timeoutId);
47
+ globalThis.window.removeEventListener("message", messageHandler);
48
+ };
49
+ globalThis.window.addEventListener("message", messageHandler);
50
+ try {
51
+ globalThis.window.parent.postMessage(
52
+ { type: "seaverse:get_token" },
53
+ "*"
54
+ // Allow any origin, supports cross-domain scenarios
55
+ );
56
+ } catch (e) {
57
+ cleanup();
58
+ resolve(null);
59
+ }
60
+ });
61
+ }
14
62
  function extractAppId() {
15
63
  if (typeof globalThis !== "undefined" && "location" in globalThis) {
16
64
  const location = globalThis.location;
@@ -28,17 +76,27 @@ var HTTPClient = class {
28
76
  headers;
29
77
  fetchFn;
30
78
  timeout;
31
- constructor(config) {
79
+ tokenPromise = null;
80
+ constructor(config, token) {
32
81
  this.baseUrl = (config.url || "https://dataservice-api.seaverse.ai").replace(/\/$/, "");
33
82
  this.fetchFn = config.options?.fetch || globalThis.fetch;
34
83
  this.timeout = config.options?.timeout || 3e4;
35
- const token = config.token.startsWith("Bearer ") ? config.token : `Bearer ${config.token}`;
36
84
  this.headers = {
37
- "Authorization": token,
38
85
  "Content-Type": "application/json",
39
86
  "Prefer": "return=representation",
40
87
  ...config.options?.headers
41
88
  };
89
+ if (token) {
90
+ const authToken = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
91
+ this.headers["Authorization"] = authToken;
92
+ }
93
+ }
94
+ /**
95
+ * Set authentication token (called after async token fetch)
96
+ */
97
+ setToken(token) {
98
+ const authToken = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
99
+ this.headers["Authorization"] = authToken;
42
100
  }
43
101
  /**
44
102
  * Make HTTP request with timeout and error handling
@@ -258,30 +316,6 @@ var CollectionImpl = class {
258
316
  `/user_data?id=eq.${id}&app_id=eq.${this.appId}&collection_name=eq.${this.collectionName}`
259
317
  );
260
318
  }
261
- async deleteAll() {
262
- const records = await this.client.get(
263
- `/user_data?app_id=eq.${this.appId}&collection_name=eq.${this.collectionName}`
264
- );
265
- const count = records.length;
266
- if (count > 0) {
267
- await this.client.delete(
268
- `/user_data?app_id=eq.${this.appId}&collection_name=eq.${this.collectionName}`
269
- );
270
- }
271
- return count;
272
- }
273
- async softDelete(id) {
274
- const response = await this.client.post("/rpc/soft_delete_user_data", {
275
- p_id: id
276
- });
277
- return response;
278
- }
279
- async restore(id) {
280
- const response = await this.client.post("/rpc/restore_user_data", {
281
- p_id: id
282
- });
283
- return response;
284
- }
285
319
  async search(criteria) {
286
320
  return this.client.get(this.tablePath, {
287
321
  app_id: `eq.${this.appId}`,
@@ -329,9 +363,9 @@ var DataServiceClientImpl = class {
329
363
  client;
330
364
  userData;
331
365
  appId;
332
- constructor(config) {
333
- this.client = new HTTPClient(config);
334
- this.appId = extractAppId();
366
+ constructor(client, appId) {
367
+ this.client = client;
368
+ this.appId = appId;
335
369
  this.userData = new DataTableImpl(this.client, "/user_data", this.appId);
336
370
  }
337
371
  async getStats() {
@@ -342,8 +376,22 @@ var DataServiceClientImpl = class {
342
376
  return this.client.post("/rpc/health");
343
377
  }
344
378
  };
345
- function createClient(config) {
346
- return new DataServiceClientImpl(config);
379
+ async function createClient(config = {}) {
380
+ let token = null;
381
+ if (debugToken) {
382
+ token = debugToken;
383
+ } else {
384
+ try {
385
+ const timeout = config.options?.tokenFetchTimeout || 5e3;
386
+ token = await getTokenFromParent(timeout);
387
+ } catch (error) {
388
+ console.warn("[SeaVerse DataService SDK] Failed to fetch token:", error);
389
+ token = null;
390
+ }
391
+ }
392
+ const httpClient = new HTTPClient(config, token || void 0);
393
+ const appId = extractAppId();
394
+ return new DataServiceClientImpl(httpClient, appId);
347
395
  }
348
396
 
349
397
  // src/index.ts
@@ -351,5 +399,6 @@ var VERSION = "1.0.0";
351
399
  export {
352
400
  DataServiceError,
353
401
  VERSION,
354
- createClient
402
+ createClient,
403
+ debugSetToken
355
404
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seaverse/dataservice",
3
- "version": "1.3.0",
3
+ "version": "1.6.0",
4
4
  "description": "AI-Friendly Universal Data Storage SDK for TypeScript/JavaScript",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",