@seaverse/dataservice 1.2.0 → 1.5.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,33 +22,46 @@ npm install @seaverse/dataservice
22
22
  ```typescript
23
23
  import { createClient } from '@seaverse/dataservice';
24
24
 
25
- // Create client (uses default SeaVerse API, appId auto-extracted from URL)
26
- const client = createClient({
27
- token: 'Bearer your-jwt-token',
25
+ // Option 1: Auto-fetch token from parent page (iframe only)
26
+ const client = await createClient({});
27
+
28
+ // Option 2: Provide token explicitly
29
+ const client = await createClient({
30
+ token: 'your-jwt-token',
28
31
  });
29
32
 
30
- // Access user data table
31
- const orders = client.userData.collection('orders');
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,
39
+ });
32
40
 
33
- // Insert data
34
- const order = await orders.insert({
41
+ // Update preferences
42
+ await userPrefs.patch(userPrefs.id, { theme: 'light' });
43
+
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();
51
+ const order2 = await client.userData.collection('order_002').insert({
52
+ order_number: 'ORD-002',
53
+ status: 'shipped',
54
+ total: 149.99,
55
+ });
46
56
 
47
- // Update data
48
- await orders.patch(order.id, { status: 'completed' });
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
+ ]);
49
62
 
50
- // Delete data
51
- await orders.delete(order.id);
63
+ // Delete a specific record
64
+ await client.userData.collection('order_001').delete(order1.id);
52
65
  ```
53
66
 
54
67
  ## Core Concepts
@@ -91,22 +104,144 @@ The SDK provides access to different data tables with different permission scope
91
104
 
92
105
  ### Collections
93
106
 
94
- 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.
95
110
 
96
- **Important**: To store multiple records, use unique collection names:
111
+ **To store multiple records, you MUST use unique collection names**:
97
112
 
98
113
  ```typescript
99
- // ✓ Correct: Unique collection names
100
- await client.userData.collection('order_1').insert(order1);
101
- 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);
102
118
 
103
- // ✓ Or use batch insert helper
104
- await client.userData.batchInsert('order', [order1, order2]);
105
- // 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
106
122
 
107
- // ✗ Wrong: Same collection name
108
- await client.userData.collection('order').insert(order1);
109
- 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);
110
245
  ```
111
246
 
112
247
  ### Data Records
@@ -133,28 +268,54 @@ interface DataRecord<T> {
133
268
  ```typescript
134
269
  import { createClient } from '@seaverse/dataservice';
135
270
 
136
- const client = createClient({
137
- token: string; // JWT token (must contain user_id)
271
+ const client = await createClient({
272
+ token?: string; // JWT token (optional, auto-fetched from parent if in iframe)
138
273
  url?: string; // PostgREST API URL (default: https://dataservice-api.seaverse.ai)
139
274
  options?: {
140
275
  timeout?: number; // Request timeout in ms (default: 30000)
276
+ tokenFetchTimeout?: number; // Token fetch timeout in ms (default: 5000)
141
277
  headers?: Record<string, string>; // Additional headers
142
278
  };
143
279
  });
144
280
  ```
145
281
 
146
- **Examples:**
282
+ **Token Options:**
283
+
284
+ The SDK supports two ways to provide authentication:
285
+
286
+ 1. **Auto-fetch from parent page (iframe only)**: When running in an iframe, the SDK can automatically request the token from the parent page via PostMessage:
147
287
 
148
288
  ```typescript
149
- // Use default SeaVerse API
150
- const client = createClient({
289
+ // No token needed - auto-fetches from parent
290
+ const client = await createClient({});
291
+
292
+ // Parent page should respond to PostMessage:
293
+ // Send: { type: 'seaverse:get_token' }
294
+ // Receive: { type: 'seaverse:token', payload: { accessToken: string, expiresIn: number } }
295
+ // Error: { type: 'seaverse:error', error: string }
296
+ ```
297
+
298
+ 2. **Explicit token**: Provide the token directly (Bearer prefix is auto-added):
299
+
300
+ ```typescript
301
+ // Token without Bearer prefix
302
+ const client = await createClient({
303
+ token: 'your-jwt-token',
304
+ });
305
+
306
+ // Or with explicit Bearer prefix (both work)
307
+ const client = await createClient({
151
308
  token: 'Bearer your-jwt-token',
152
309
  });
310
+ ```
311
+
312
+ **Custom Endpoint:**
153
313
 
314
+ ```typescript
154
315
  // Use custom API endpoint
155
- const client = createClient({
316
+ const client = await createClient({
156
317
  url: 'https://your-custom-api.example.com',
157
- token: 'Bearer your-jwt-token',
318
+ token: 'your-jwt-token',
158
319
  });
159
320
  ```
160
321
 
@@ -213,14 +374,8 @@ collection.patch(id: string, partial: Partial<T>): Promise<DataRecord<T>>
213
374
  #### Delete
214
375
 
215
376
  ```typescript
216
- // Hard delete (permanent)
377
+ // Delete a record by ID (permanent deletion)
217
378
  collection.delete(id: string): Promise<void>
218
-
219
- // Soft delete (sets deleted_at timestamp)
220
- collection.softDelete(id: string): Promise<boolean>
221
-
222
- // Restore soft-deleted record
223
- collection.restore(id: string): Promise<boolean>
224
379
  ```
225
380
 
226
381
  ### Query Builder
@@ -302,11 +457,10 @@ const client = createClient({
302
457
  token: process.env.JWT_TOKEN!,
303
458
  });
304
459
 
305
- const orders = client.userData.collection<Order>('orders');
306
-
307
- // Create order
308
- const order = await orders.insert({
309
- order_number: `ORD-${Date.now()}`,
460
+ // Create multiple orders - each with unique collection name
461
+ const orderNumber1 = `ORD-${Date.now()}`;
462
+ const order1 = await client.userData.collection<Order>(`order_${orderNumber1}`).insert({
463
+ order_number: orderNumber1,
310
464
  customer_email: 'customer@example.com',
311
465
  items: [
312
466
  { product_id: 'PROD-001', quantity: 2, price: 29.99 },
@@ -316,26 +470,36 @@ const order = await orders.insert({
316
470
  total: 109.97,
317
471
  });
318
472
 
319
- console.log('Order created:', order.id);
473
+ console.log('Order created:', order1.id, 'Collection:', `order_${orderNumber1}`);
474
+
475
+ // Create another order
476
+ const orderNumber2 = `ORD-${Date.now() + 1}`;
477
+ const order2 = await client.userData.collection<Order>(`order_${orderNumber2}`).insert({
478
+ order_number: orderNumber2,
479
+ customer_email: 'customer@example.com',
480
+ items: [
481
+ { product_id: 'PROD-003', quantity: 1, price: 199.99 },
482
+ ],
483
+ status: 'pending',
484
+ total: 199.99,
485
+ });
320
486
 
321
- // Query pending orders over $50
322
- const pendingOrders = await orders
323
- .select()
324
- .eq('data->>status', 'pending')
325
- .gt('data->total', '50')
326
- .order('created_at', { descending: true })
327
- .limit(10)
328
- .execute();
487
+ // Update order status (access by collection name)
488
+ await client.userData.collection(`order_${orderNumber1}`).patch(order1.id, {
489
+ status: 'shipped'
490
+ });
329
491
 
330
- console.log(`Found ${pendingOrders.length} pending orders`);
492
+ // Get specific order
493
+ const retrieved = await client.userData.collection(`order_${orderNumber1}`).get(order1.id);
494
+ console.log('Order status:', retrieved?.data.status);
331
495
 
332
- // Update order status
333
- await orders.patch(order.id, { status: 'shipped' });
496
+ // Delete a specific order
497
+ await client.userData.collection(`order_${orderNumber1}`).delete(order1.id);
334
498
 
335
- // Search by customer email
336
- const customerOrders = await orders.search({
337
- customer_email: 'customer@example.com',
338
- });
499
+ // Note: To query across multiple orders, you would need to:
500
+ // 1. Store order metadata in a separate tracking collection
501
+ // 2. Or use a naming convention and iterate through known order IDs
502
+ // 3. Or use the batchInsert pattern and track the generated collection names
339
503
  ```
340
504
 
341
505
  ### Chat Conversations
@@ -357,10 +521,9 @@ type Conversation = {
357
521
  };
358
522
  };
359
523
 
360
- const conversations = client.userData.collection<Conversation>('chats');
361
-
362
- // Create conversation
363
- const conv = await conversations.insert({
524
+ // Create a new conversation with unique ID
525
+ const conversationId = `conv_${Date.now()}`;
526
+ const conv = await client.userData.collection<Conversation>(conversationId).insert({
364
527
  title: 'Project Planning Discussion',
365
528
  model: 'claude-3-opus',
366
529
  messages: [
@@ -372,10 +535,12 @@ const conv = await conversations.insert({
372
535
  ],
373
536
  });
374
537
 
375
- // Add assistant response
376
- const current = await conversations.get(conv.id);
538
+ console.log('Conversation created:', conversationId);
539
+
540
+ // Add assistant response to existing conversation
541
+ const current = await client.userData.collection<Conversation>(conversationId).get(conv.id);
377
542
  if (current) {
378
- await conversations.patch(conv.id, {
543
+ await client.userData.collection<Conversation>(conversationId).patch(conv.id, {
379
544
  messages: [
380
545
  ...current.data.messages,
381
546
  {
@@ -387,12 +552,23 @@ if (current) {
387
552
  });
388
553
  }
389
554
 
390
- // List recent conversations
391
- const recent = await conversations
392
- .select()
393
- .order('updated_at', { descending: true })
394
- .limit(20)
395
- .execute();
555
+ // Store individual messages separately (alternative pattern)
556
+ // Each message gets its own collection
557
+ const msgId1 = await client.userData.collection(`${conversationId}_msg_1`).insert({
558
+ role: 'user',
559
+ content: 'Help me plan a new feature',
560
+ timestamp: new Date().toISOString(),
561
+ });
562
+
563
+ const msgId2 = await client.userData.collection(`${conversationId}_msg_2`).insert({
564
+ role: 'assistant',
565
+ content: 'I can help with that!',
566
+ timestamp: new Date().toISOString(),
567
+ });
568
+
569
+ // Get a specific conversation
570
+ const retrieved = await client.userData.collection(conversationId).get(conv.id);
571
+ console.log('Conversation title:', retrieved?.data.title);
396
572
  ```
397
573
 
398
574
  ### User Preferences
package/dist/index.d.mts CHANGED
@@ -19,8 +19,11 @@
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;
22
+ /**
23
+ * JWT token containing user_id in payload.user_id
24
+ * If not provided, will attempt to fetch from parent page via PostMessage (iframe only)
25
+ */
26
+ token?: string;
24
27
  /** Optional configuration */
25
28
  options?: {
26
29
  /** Custom fetch implementation (useful for Node.js < 18) */
@@ -29,6 +32,8 @@ interface ClientConfig {
29
32
  headers?: Record<string, string>;
30
33
  /** Request timeout in milliseconds (default: 30000) */
31
34
  timeout?: number;
35
+ /** Timeout for fetching token from parent (milliseconds, default: 5000) */
36
+ tokenFetchTimeout?: number;
32
37
  };
33
38
  }
34
39
  /**
@@ -193,12 +198,8 @@ interface Collection<T = any> {
193
198
  update(id: string, data: T): Promise<DataRecord<T>>;
194
199
  /** Patch a record by ID (merges with existing data) */
195
200
  patch(id: string, partial: Partial<T>): Promise<DataRecord<T>>;
196
- /** Hard delete a record by ID */
201
+ /** Delete a record by ID (permanent) */
197
202
  delete(id: string): Promise<void>;
198
- /** Soft delete a record by ID (sets deleted_at) */
199
- softDelete(id: string): Promise<boolean>;
200
- /** Restore a soft-deleted record */
201
- restore(id: string): Promise<boolean>;
202
203
  /** Search records by JSONB contains */
203
204
  search(criteria: Partial<T>): Promise<DataRecord<T>[]>;
204
205
  /** Count records in collection */
@@ -244,34 +245,59 @@ interface DataServiceClient {
244
245
  * 4. Secure: RLS enforced, token-based auth
245
246
  */
246
247
 
248
+ /**
249
+ * Set debug token for testing/debugging
250
+ * This allows you to bypass the token fetch logic during development
251
+ *
252
+ * @param token The token to use for debugging
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * import { debugSetToken, createClient } from '@seaverse/dataservice';
257
+ *
258
+ * // Set debug token before creating client
259
+ * debugSetToken('your-test-token');
260
+ *
261
+ * // Client will use debug token instead of fetching
262
+ * const client = await createClient({});
263
+ * ```
264
+ */
265
+ declare function debugSetToken(token: string): void;
247
266
  /**
248
267
  * Create a new Data Service client
249
268
  *
250
269
  * AI-friendly: Single entry point with clear configuration
251
270
  *
252
- * @param config - Client configuration with JWT token (URL defaults to https://dataservice-api.seaverse.ai)
271
+ * @param config - Client configuration (token optional, will auto-fetch from parent if in iframe)
253
272
  * @returns DataServiceClient instance
254
273
  *
255
274
  * @example
256
275
  * ```typescript
257
- * // Use default SeaVerse API endpoint
258
- * const client = createClient({
276
+ * // Auto-fetch token from parent page (iframe only)
277
+ * const client = await createClient({});
278
+ *
279
+ * // Or provide token explicitly
280
+ * const client = await createClient({
281
+ * token: 'your-jwt-token-here',
282
+ * });
283
+ *
284
+ * // Bearer prefix is auto-added
285
+ * const client = await createClient({
259
286
  * token: 'Bearer your-jwt-token-here',
260
287
  * });
261
288
  *
262
- * // Or specify custom endpoint
263
- * const client = createClient({
289
+ * // Custom endpoint
290
+ * const client = await createClient({
264
291
  * url: 'https://your-postgrest-api.example.com',
265
- * token: 'Bearer your-jwt-token-here',
292
+ * token: 'your-jwt-token-here',
266
293
  * });
267
294
  *
268
295
  * // appId is automatically extracted from current URL
269
- * // Direct access to collections - clean and simple
270
296
  * const order = await client.userData.collection('orders').insert({ ... });
271
297
  * const orders = await client.userData.collection('orders').select().execute();
272
298
  * ```
273
299
  */
274
- declare function createClient(config: ClientConfig): DataServiceClient;
300
+ declare function createClient(config?: ClientConfig): Promise<DataServiceClient>;
275
301
 
276
302
  /**
277
303
  * SeaVerse Data Service SDK
@@ -280,18 +306,21 @@ declare function createClient(config: ClientConfig): DataServiceClient;
280
306
  *
281
307
  * @packageDocumentation
282
308
  *
283
- * @example Basic Usage
309
+ * @example Basic Usage with Auto Token Fetch
284
310
  * ```typescript
285
311
  * import { createClient } from '@seaverse/dataservice';
286
312
  *
287
- * // Create client (uses default SeaVerse API)
288
- * const client = createClient({
289
- * token: 'Bearer your-jwt-token',
313
+ * // Auto-fetch token from parent page (iframe only)
314
+ * const client = await createClient({});
315
+ *
316
+ * // Or provide token explicitly
317
+ * const client = await createClient({
318
+ * token: 'your-jwt-token',
290
319
  * });
291
320
  *
292
321
  * // appId is automatically extracted from current URL
293
322
  * // Insert data
294
- * const order = await client.userData.collection('orders').insert({
323
+ * const order = await client.userData.collection('order_001').insert({
295
324
  * order_number: 'ORD-123',
296
325
  * status: 'pending',
297
326
  * total: 99.99,
@@ -299,7 +328,7 @@ declare function createClient(config: ClientConfig): DataServiceClient;
299
328
  *
300
329
  * // Query data
301
330
  * const orders = await client.userData
302
- * .collection('orders')
331
+ * .collection('order_001')
303
332
  * .select()
304
333
  * .eq('data->>status', 'pending')
305
334
  * .order('created_at', { descending: true })
@@ -310,4 +339,4 @@ declare function createClient(config: ClientConfig): DataServiceClient;
310
339
 
311
340
  declare const VERSION = "1.0.0";
312
341
 
313
- export { type APIError, type ClientConfig, type Collection, type DataRecord, type DataServiceClient, DataServiceError, type DataTable, type OrderOptions, type QueryBuilder, type QueryFilter, type UserDataStats, VERSION, createClient };
342
+ 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
@@ -19,8 +19,11 @@
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;
22
+ /**
23
+ * JWT token containing user_id in payload.user_id
24
+ * If not provided, will attempt to fetch from parent page via PostMessage (iframe only)
25
+ */
26
+ token?: string;
24
27
  /** Optional configuration */
25
28
  options?: {
26
29
  /** Custom fetch implementation (useful for Node.js < 18) */
@@ -29,6 +32,8 @@ interface ClientConfig {
29
32
  headers?: Record<string, string>;
30
33
  /** Request timeout in milliseconds (default: 30000) */
31
34
  timeout?: number;
35
+ /** Timeout for fetching token from parent (milliseconds, default: 5000) */
36
+ tokenFetchTimeout?: number;
32
37
  };
33
38
  }
34
39
  /**
@@ -193,12 +198,8 @@ interface Collection<T = any> {
193
198
  update(id: string, data: T): Promise<DataRecord<T>>;
194
199
  /** Patch a record by ID (merges with existing data) */
195
200
  patch(id: string, partial: Partial<T>): Promise<DataRecord<T>>;
196
- /** Hard delete a record by ID */
201
+ /** Delete a record by ID (permanent) */
197
202
  delete(id: string): Promise<void>;
198
- /** Soft delete a record by ID (sets deleted_at) */
199
- softDelete(id: string): Promise<boolean>;
200
- /** Restore a soft-deleted record */
201
- restore(id: string): Promise<boolean>;
202
203
  /** Search records by JSONB contains */
203
204
  search(criteria: Partial<T>): Promise<DataRecord<T>[]>;
204
205
  /** Count records in collection */
@@ -244,34 +245,59 @@ interface DataServiceClient {
244
245
  * 4. Secure: RLS enforced, token-based auth
245
246
  */
246
247
 
248
+ /**
249
+ * Set debug token for testing/debugging
250
+ * This allows you to bypass the token fetch logic during development
251
+ *
252
+ * @param token The token to use for debugging
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * import { debugSetToken, createClient } from '@seaverse/dataservice';
257
+ *
258
+ * // Set debug token before creating client
259
+ * debugSetToken('your-test-token');
260
+ *
261
+ * // Client will use debug token instead of fetching
262
+ * const client = await createClient({});
263
+ * ```
264
+ */
265
+ declare function debugSetToken(token: string): void;
247
266
  /**
248
267
  * Create a new Data Service client
249
268
  *
250
269
  * AI-friendly: Single entry point with clear configuration
251
270
  *
252
- * @param config - Client configuration with JWT token (URL defaults to https://dataservice-api.seaverse.ai)
271
+ * @param config - Client configuration (token optional, will auto-fetch from parent if in iframe)
253
272
  * @returns DataServiceClient instance
254
273
  *
255
274
  * @example
256
275
  * ```typescript
257
- * // Use default SeaVerse API endpoint
258
- * const client = createClient({
276
+ * // Auto-fetch token from parent page (iframe only)
277
+ * const client = await createClient({});
278
+ *
279
+ * // Or provide token explicitly
280
+ * const client = await createClient({
281
+ * token: 'your-jwt-token-here',
282
+ * });
283
+ *
284
+ * // Bearer prefix is auto-added
285
+ * const client = await createClient({
259
286
  * token: 'Bearer your-jwt-token-here',
260
287
  * });
261
288
  *
262
- * // Or specify custom endpoint
263
- * const client = createClient({
289
+ * // Custom endpoint
290
+ * const client = await createClient({
264
291
  * url: 'https://your-postgrest-api.example.com',
265
- * token: 'Bearer your-jwt-token-here',
292
+ * token: 'your-jwt-token-here',
266
293
  * });
267
294
  *
268
295
  * // appId is automatically extracted from current URL
269
- * // Direct access to collections - clean and simple
270
296
  * const order = await client.userData.collection('orders').insert({ ... });
271
297
  * const orders = await client.userData.collection('orders').select().execute();
272
298
  * ```
273
299
  */
274
- declare function createClient(config: ClientConfig): DataServiceClient;
300
+ declare function createClient(config?: ClientConfig): Promise<DataServiceClient>;
275
301
 
276
302
  /**
277
303
  * SeaVerse Data Service SDK
@@ -280,18 +306,21 @@ declare function createClient(config: ClientConfig): DataServiceClient;
280
306
  *
281
307
  * @packageDocumentation
282
308
  *
283
- * @example Basic Usage
309
+ * @example Basic Usage with Auto Token Fetch
284
310
  * ```typescript
285
311
  * import { createClient } from '@seaverse/dataservice';
286
312
  *
287
- * // Create client (uses default SeaVerse API)
288
- * const client = createClient({
289
- * token: 'Bearer your-jwt-token',
313
+ * // Auto-fetch token from parent page (iframe only)
314
+ * const client = await createClient({});
315
+ *
316
+ * // Or provide token explicitly
317
+ * const client = await createClient({
318
+ * token: 'your-jwt-token',
290
319
  * });
291
320
  *
292
321
  * // appId is automatically extracted from current URL
293
322
  * // Insert data
294
- * const order = await client.userData.collection('orders').insert({
323
+ * const order = await client.userData.collection('order_001').insert({
295
324
  * order_number: 'ORD-123',
296
325
  * status: 'pending',
297
326
  * total: 99.99,
@@ -299,7 +328,7 @@ declare function createClient(config: ClientConfig): DataServiceClient;
299
328
  *
300
329
  * // Query data
301
330
  * const orders = await client.userData
302
- * .collection('orders')
331
+ * .collection('order_001')
303
332
  * .select()
304
333
  * .eq('data->>status', 'pending')
305
334
  * .order('created_at', { descending: true })
@@ -310,4 +339,4 @@ declare function createClient(config: ClientConfig): DataServiceClient;
310
339
 
311
340
  declare const VERSION = "1.0.0";
312
341
 
313
- export { type APIError, type ClientConfig, type Collection, type DataRecord, type DataServiceClient, DataServiceError, type DataTable, type OrderOptions, type QueryBuilder, type QueryFilter, type UserDataStats, VERSION, createClient };
342
+ 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,16 +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
113
  this.headers = {
64
- "Authorization": config.token,
65
114
  "Content-Type": "application/json",
66
115
  "Prefer": "return=representation",
67
116
  ...config.options?.headers
68
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;
69
129
  }
70
130
  /**
71
131
  * Make HTTP request with timeout and error handling
@@ -285,18 +345,6 @@ var CollectionImpl = class {
285
345
  `/user_data?id=eq.${id}&app_id=eq.${this.appId}&collection_name=eq.${this.collectionName}`
286
346
  );
287
347
  }
288
- async softDelete(id) {
289
- const response = await this.client.post("/rpc/soft_delete_user_data", {
290
- p_id: id
291
- });
292
- return response;
293
- }
294
- async restore(id) {
295
- const response = await this.client.post("/rpc/restore_user_data", {
296
- p_id: id
297
- });
298
- return response;
299
- }
300
348
  async search(criteria) {
301
349
  return this.client.get(this.tablePath, {
302
350
  app_id: `eq.${this.appId}`,
@@ -344,9 +392,9 @@ var DataServiceClientImpl = class {
344
392
  client;
345
393
  userData;
346
394
  appId;
347
- constructor(config) {
348
- this.client = new HTTPClient(config);
349
- this.appId = extractAppId();
395
+ constructor(client, appId) {
396
+ this.client = client;
397
+ this.appId = appId;
350
398
  this.userData = new DataTableImpl(this.client, "/user_data", this.appId);
351
399
  }
352
400
  async getStats() {
@@ -357,8 +405,26 @@ var DataServiceClientImpl = class {
357
405
  return this.client.post("/rpc/health");
358
406
  }
359
407
  };
360
- function createClient(config) {
361
- return new DataServiceClientImpl(config);
408
+ async function createClient(config = {}) {
409
+ let token = config.token;
410
+ if (!token) {
411
+ if (debugToken) {
412
+ token = debugToken;
413
+ } else {
414
+ const timeout = config.options?.tokenFetchTimeout || 5e3;
415
+ const fetchedToken = await getTokenFromParent(timeout);
416
+ if (!fetchedToken) {
417
+ throw new DataServiceError(
418
+ "No token provided and failed to fetch from parent page. Please provide a token or ensure the app is running in an iframe with a parent that responds to seaverse:get_token messages.",
419
+ "NO_TOKEN"
420
+ );
421
+ }
422
+ token = fetchedToken;
423
+ }
424
+ }
425
+ const httpClient = new HTTPClient(config, token);
426
+ const appId = extractAppId();
427
+ return new DataServiceClientImpl(httpClient, appId);
362
428
  }
363
429
 
364
430
  // src/index.ts
@@ -367,5 +433,6 @@ var VERSION = "1.0.0";
367
433
  0 && (module.exports = {
368
434
  DataServiceError,
369
435
  VERSION,
370
- createClient
436
+ createClient,
437
+ debugSetToken
371
438
  });
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,16 +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
84
  this.headers = {
36
- "Authorization": config.token,
37
85
  "Content-Type": "application/json",
38
86
  "Prefer": "return=representation",
39
87
  ...config.options?.headers
40
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;
41
100
  }
42
101
  /**
43
102
  * Make HTTP request with timeout and error handling
@@ -257,18 +316,6 @@ var CollectionImpl = class {
257
316
  `/user_data?id=eq.${id}&app_id=eq.${this.appId}&collection_name=eq.${this.collectionName}`
258
317
  );
259
318
  }
260
- async softDelete(id) {
261
- const response = await this.client.post("/rpc/soft_delete_user_data", {
262
- p_id: id
263
- });
264
- return response;
265
- }
266
- async restore(id) {
267
- const response = await this.client.post("/rpc/restore_user_data", {
268
- p_id: id
269
- });
270
- return response;
271
- }
272
319
  async search(criteria) {
273
320
  return this.client.get(this.tablePath, {
274
321
  app_id: `eq.${this.appId}`,
@@ -316,9 +363,9 @@ var DataServiceClientImpl = class {
316
363
  client;
317
364
  userData;
318
365
  appId;
319
- constructor(config) {
320
- this.client = new HTTPClient(config);
321
- this.appId = extractAppId();
366
+ constructor(client, appId) {
367
+ this.client = client;
368
+ this.appId = appId;
322
369
  this.userData = new DataTableImpl(this.client, "/user_data", this.appId);
323
370
  }
324
371
  async getStats() {
@@ -329,8 +376,26 @@ var DataServiceClientImpl = class {
329
376
  return this.client.post("/rpc/health");
330
377
  }
331
378
  };
332
- function createClient(config) {
333
- return new DataServiceClientImpl(config);
379
+ async function createClient(config = {}) {
380
+ let token = config.token;
381
+ if (!token) {
382
+ if (debugToken) {
383
+ token = debugToken;
384
+ } else {
385
+ const timeout = config.options?.tokenFetchTimeout || 5e3;
386
+ const fetchedToken = await getTokenFromParent(timeout);
387
+ if (!fetchedToken) {
388
+ throw new DataServiceError(
389
+ "No token provided and failed to fetch from parent page. Please provide a token or ensure the app is running in an iframe with a parent that responds to seaverse:get_token messages.",
390
+ "NO_TOKEN"
391
+ );
392
+ }
393
+ token = fetchedToken;
394
+ }
395
+ }
396
+ const httpClient = new HTTPClient(config, token);
397
+ const appId = extractAppId();
398
+ return new DataServiceClientImpl(httpClient, appId);
334
399
  }
335
400
 
336
401
  // src/index.ts
@@ -338,5 +403,6 @@ var VERSION = "1.0.0";
338
403
  export {
339
404
  DataServiceError,
340
405
  VERSION,
341
- createClient
406
+ createClient,
407
+ debugSetToken
342
408
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seaverse/dataservice",
3
- "version": "1.2.0",
3
+ "version": "1.5.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",