@seaverse/dataservice 1.3.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,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({
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({
27
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();
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.
110
+
111
+ **To store multiple records, you MUST use unique collection names**:
112
+
113
+ ```typescript
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);
118
+
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
122
+
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
+ ```
98
216
 
99
- **Important**: To store multiple records, use unique collection names:
217
+ ### Pattern 3: Batch Insert for Multiple Records
218
+
219
+ Use this when creating multiple records at once:
100
220
 
101
221
  ```typescript
102
- // Correct: Unique collection names
103
- await client.userData.collection('order_1').insert(order1);
104
- await client.userData.collection('order_2').insert(order2);
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
105
234
 
106
- // Or use batch insert helper
107
- await client.userData.batchInsert('order', [order1, order2]);
108
- // Creates: order_0, order_1, order_2, ...
235
+ Use this for nested data structures:
109
236
 
110
- // ✗ Wrong: Same collection name
111
- await client.userData.collection('order').insert(order1);
112
- await client.userData.collection('order').insert(order2); // Error: 409 Conflict
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,31 +268,52 @@ 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({
272
+ token?: string; // JWT token (optional, auto-fetched from parent if in iframe)
141
273
  url?: string; // PostgREST API URL (default: https://dataservice-api.seaverse.ai)
142
274
  options?: {
143
275
  timeout?: number; // Request timeout in ms (default: 30000)
276
+ tokenFetchTimeout?: number; // Token fetch timeout in ms (default: 5000)
144
277
  headers?: Record<string, string>; // Additional headers
145
278
  };
146
279
  });
147
280
  ```
148
281
 
149
- **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:
150
287
 
151
288
  ```typescript
152
- // Use default SeaVerse API (Bearer prefix auto-added)
153
- 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({
154
303
  token: 'your-jwt-token',
155
304
  });
156
305
 
157
306
  // Or with explicit Bearer prefix (both work)
158
- const client = createClient({
307
+ const client = await createClient({
159
308
  token: 'Bearer your-jwt-token',
160
309
  });
310
+ ```
311
+
312
+ **Custom Endpoint:**
161
313
 
314
+ ```typescript
162
315
  // Use custom API endpoint
163
- const client = createClient({
316
+ const client = await createClient({
164
317
  url: 'https://your-custom-api.example.com',
165
318
  token: 'your-jwt-token',
166
319
  });
@@ -221,17 +374,8 @@ collection.patch(id: string, partial: Partial<T>): Promise<DataRecord<T>>
221
374
  #### Delete
222
375
 
223
376
  ```typescript
224
- // Hard delete single record (permanent)
377
+ // Delete a record by ID (permanent deletion)
225
378
  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
379
  ```
236
380
 
237
381
  ### Query Builder
@@ -313,11 +457,10 @@ const client = createClient({
313
457
  token: process.env.JWT_TOKEN!,
314
458
  });
315
459
 
316
- const orders = client.userData.collection<Order>('orders');
317
-
318
- // Create order
319
- const order = await orders.insert({
320
- 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,
321
464
  customer_email: 'customer@example.com',
322
465
  items: [
323
466
  { product_id: 'PROD-001', quantity: 2, price: 29.99 },
@@ -327,33 +470,36 @@ const order = await orders.insert({
327
470
  total: 109.97,
328
471
  });
329
472
 
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`);
342
-
343
- // Update order status
344
- await orders.patch(order.id, { status: 'shipped' });
473
+ console.log('Order created:', order1.id, 'Collection:', `order_${orderNumber1}`);
345
474
 
346
- // Search by customer email
347
- const customerOrders = await orders.search({
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,
348
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,
349
485
  });
350
486
 
487
+ // Update order status (access by collection name)
488
+ await client.userData.collection(`order_${orderNumber1}`).patch(order1.id, {
489
+ status: 'shipped'
490
+ });
491
+
492
+ // Get specific order
493
+ const retrieved = await client.userData.collection(`order_${orderNumber1}`).get(order1.id);
494
+ console.log('Order status:', retrieved?.data.status);
495
+
351
496
  // Delete a specific order
352
- await orders.delete(order.id);
497
+ await client.userData.collection(`order_${orderNumber1}`).delete(order1.id);
353
498
 
354
- // Delete all orders (useful for cleanup)
355
- const deletedCount = await orders.deleteAll();
356
- console.log(`Deleted ${deletedCount} orders`);
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
357
503
  ```
358
504
 
359
505
  ### Chat Conversations
@@ -375,10 +521,9 @@ type Conversation = {
375
521
  };
376
522
  };
377
523
 
378
- const conversations = client.userData.collection<Conversation>('chats');
379
-
380
- // Create conversation
381
- 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({
382
527
  title: 'Project Planning Discussion',
383
528
  model: 'claude-3-opus',
384
529
  messages: [
@@ -390,10 +535,12 @@ const conv = await conversations.insert({
390
535
  ],
391
536
  });
392
537
 
393
- // Add assistant response
394
- 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);
395
542
  if (current) {
396
- await conversations.patch(conv.id, {
543
+ await client.userData.collection<Conversation>(conversationId).patch(conv.id, {
397
544
  messages: [
398
545
  ...current.data.messages,
399
546
  {
@@ -405,12 +552,23 @@ if (current) {
405
552
  });
406
553
  }
407
554
 
408
- // List recent conversations
409
- const recent = await conversations
410
- .select()
411
- .order('updated_at', { descending: true })
412
- .limit(20)
413
- .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);
414
572
  ```
415
573
 
416
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,14 +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
- /** 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
203
  /** Search records by JSONB contains */
205
204
  search(criteria: Partial<T>): Promise<DataRecord<T>[]>;
206
205
  /** Count records in collection */
@@ -246,39 +245,59 @@ interface DataServiceClient {
246
245
  * 4. Secure: RLS enforced, token-based auth
247
246
  */
248
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;
249
266
  /**
250
267
  * Create a new Data Service client
251
268
  *
252
269
  * AI-friendly: Single entry point with clear configuration
253
270
  *
254
- * @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)
255
272
  * @returns DataServiceClient instance
256
273
  *
257
274
  * @example
258
275
  * ```typescript
259
- * // Use default SeaVerse API endpoint (Bearer prefix auto-added)
260
- * 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({
261
281
  * token: 'your-jwt-token-here',
262
282
  * });
263
283
  *
264
- * // Or with explicit Bearer prefix (both work)
265
- * const client = createClient({
284
+ * // Bearer prefix is auto-added
285
+ * const client = await createClient({
266
286
  * token: 'Bearer your-jwt-token-here',
267
287
  * });
268
288
  *
269
- * // Or specify custom endpoint
270
- * const client = createClient({
289
+ * // Custom endpoint
290
+ * const client = await createClient({
271
291
  * url: 'https://your-postgrest-api.example.com',
272
292
  * token: 'your-jwt-token-here',
273
293
  * });
274
294
  *
275
295
  * // appId is automatically extracted from current URL
276
- * // Direct access to collections - clean and simple
277
296
  * const order = await client.userData.collection('orders').insert({ ... });
278
297
  * const orders = await client.userData.collection('orders').select().execute();
279
298
  * ```
280
299
  */
281
- declare function createClient(config: ClientConfig): DataServiceClient;
300
+ declare function createClient(config?: ClientConfig): Promise<DataServiceClient>;
282
301
 
283
302
  /**
284
303
  * SeaVerse Data Service SDK
@@ -287,18 +306,21 @@ declare function createClient(config: ClientConfig): DataServiceClient;
287
306
  *
288
307
  * @packageDocumentation
289
308
  *
290
- * @example Basic Usage
309
+ * @example Basic Usage with Auto Token Fetch
291
310
  * ```typescript
292
311
  * import { createClient } from '@seaverse/dataservice';
293
312
  *
294
- * // Create client (Bearer prefix auto-added, uses default SeaVerse API)
295
- * const client = createClient({
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({
296
318
  * token: 'your-jwt-token',
297
319
  * });
298
320
  *
299
321
  * // appId is automatically extracted from current URL
300
322
  * // Insert data
301
- * const order = await client.userData.collection('orders').insert({
323
+ * const order = await client.userData.collection('order_001').insert({
302
324
  * order_number: 'ORD-123',
303
325
  * status: 'pending',
304
326
  * total: 99.99,
@@ -306,7 +328,7 @@ declare function createClient(config: ClientConfig): DataServiceClient;
306
328
  *
307
329
  * // Query data
308
330
  * const orders = await client.userData
309
- * .collection('orders')
331
+ * .collection('order_001')
310
332
  * .select()
311
333
  * .eq('data->>status', 'pending')
312
334
  * .order('created_at', { descending: true })
@@ -317,4 +339,4 @@ declare function createClient(config: ClientConfig): DataServiceClient;
317
339
 
318
340
  declare const VERSION = "1.0.0";
319
341
 
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 };
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,14 +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
- /** 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
203
  /** Search records by JSONB contains */
205
204
  search(criteria: Partial<T>): Promise<DataRecord<T>[]>;
206
205
  /** Count records in collection */
@@ -246,39 +245,59 @@ interface DataServiceClient {
246
245
  * 4. Secure: RLS enforced, token-based auth
247
246
  */
248
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;
249
266
  /**
250
267
  * Create a new Data Service client
251
268
  *
252
269
  * AI-friendly: Single entry point with clear configuration
253
270
  *
254
- * @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)
255
272
  * @returns DataServiceClient instance
256
273
  *
257
274
  * @example
258
275
  * ```typescript
259
- * // Use default SeaVerse API endpoint (Bearer prefix auto-added)
260
- * 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({
261
281
  * token: 'your-jwt-token-here',
262
282
  * });
263
283
  *
264
- * // Or with explicit Bearer prefix (both work)
265
- * const client = createClient({
284
+ * // Bearer prefix is auto-added
285
+ * const client = await createClient({
266
286
  * token: 'Bearer your-jwt-token-here',
267
287
  * });
268
288
  *
269
- * // Or specify custom endpoint
270
- * const client = createClient({
289
+ * // Custom endpoint
290
+ * const client = await createClient({
271
291
  * url: 'https://your-postgrest-api.example.com',
272
292
  * token: 'your-jwt-token-here',
273
293
  * });
274
294
  *
275
295
  * // appId is automatically extracted from current URL
276
- * // Direct access to collections - clean and simple
277
296
  * const order = await client.userData.collection('orders').insert({ ... });
278
297
  * const orders = await client.userData.collection('orders').select().execute();
279
298
  * ```
280
299
  */
281
- declare function createClient(config: ClientConfig): DataServiceClient;
300
+ declare function createClient(config?: ClientConfig): Promise<DataServiceClient>;
282
301
 
283
302
  /**
284
303
  * SeaVerse Data Service SDK
@@ -287,18 +306,21 @@ declare function createClient(config: ClientConfig): DataServiceClient;
287
306
  *
288
307
  * @packageDocumentation
289
308
  *
290
- * @example Basic Usage
309
+ * @example Basic Usage with Auto Token Fetch
291
310
  * ```typescript
292
311
  * import { createClient } from '@seaverse/dataservice';
293
312
  *
294
- * // Create client (Bearer prefix auto-added, uses default SeaVerse API)
295
- * const client = createClient({
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({
296
318
  * token: 'your-jwt-token',
297
319
  * });
298
320
  *
299
321
  * // appId is automatically extracted from current URL
300
322
  * // Insert data
301
- * const order = await client.userData.collection('orders').insert({
323
+ * const order = await client.userData.collection('order_001').insert({
302
324
  * order_number: 'ORD-123',
303
325
  * status: 'pending',
304
326
  * total: 99.99,
@@ -306,7 +328,7 @@ declare function createClient(config: ClientConfig): DataServiceClient;
306
328
  *
307
329
  * // Query data
308
330
  * const orders = await client.userData
309
- * .collection('orders')
331
+ * .collection('order_001')
310
332
  * .select()
311
333
  * .eq('data->>status', 'pending')
312
334
  * .order('created_at', { descending: true })
@@ -317,4 +339,4 @@ declare function createClient(config: ClientConfig): DataServiceClient;
317
339
 
318
340
  declare const VERSION = "1.0.0";
319
341
 
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 };
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,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,26 @@ 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 = 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);
375
428
  }
376
429
 
377
430
  // src/index.ts
@@ -380,5 +433,6 @@ var VERSION = "1.0.0";
380
433
  0 && (module.exports = {
381
434
  DataServiceError,
382
435
  VERSION,
383
- createClient
436
+ createClient,
437
+ debugSetToken
384
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,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,26 @@ 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 = 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);
347
399
  }
348
400
 
349
401
  // src/index.ts
@@ -351,5 +403,6 @@ var VERSION = "1.0.0";
351
403
  export {
352
404
  DataServiceError,
353
405
  VERSION,
354
- createClient
406
+ createClient,
407
+ debugSetToken
355
408
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seaverse/dataservice",
3
- "version": "1.3.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",