@seaverse/dataservice 1.8.4 → 1.8.5

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
@@ -471,6 +471,9 @@ collection.insert(data: T): Promise<DataRecord<T>>
471
471
 
472
472
  // Insert with custom UUID
473
473
  collection.insert(data: T, id: string): Promise<DataRecord<T>>
474
+
475
+ // Upsert with deterministic per-user UUID derived from user_id + app_id + collection name
476
+ collection.upsert(data: T): Promise<DataRecord<T>>
474
477
  ```
475
478
 
476
479
  #### Read
package/dist/index.d.mts CHANGED
@@ -183,6 +183,13 @@ interface QueryBuilder<T = any> {
183
183
  interface Collection<T = any> {
184
184
  /** Insert a new record (optionally provide UUID) */
185
185
  insert(data: T, id?: string): Promise<DataRecord<T>>;
186
+ /**
187
+ * Upsert a record using a stable per-user key.
188
+ *
189
+ * The SDK derives a deterministic UUID from the authenticated user_id,
190
+ * app_id, and collection name, then uses PostgREST on_conflict=id.
191
+ */
192
+ upsert(data: T): Promise<DataRecord<T>>;
186
193
  /** Get a single record by ID (alias for selectById) */
187
194
  get(id: string): Promise<DataRecord<T> | null>;
188
195
  /** Select records with optional filters */
@@ -239,6 +246,8 @@ interface DataTable {
239
246
  interface DataServiceClient {
240
247
  /** Automatically extracted application ID from current URL */
241
248
  readonly appId: string;
249
+ /** User ID decoded from the JWT token when available */
250
+ readonly userId: string | null;
242
251
  /** Access user private data table (user_data) */
243
252
  readonly userData: DataTable;
244
253
  /** Get user data statistics (RPC call) */
package/dist/index.d.ts CHANGED
@@ -183,6 +183,13 @@ interface QueryBuilder<T = any> {
183
183
  interface Collection<T = any> {
184
184
  /** Insert a new record (optionally provide UUID) */
185
185
  insert(data: T, id?: string): Promise<DataRecord<T>>;
186
+ /**
187
+ * Upsert a record using a stable per-user key.
188
+ *
189
+ * The SDK derives a deterministic UUID from the authenticated user_id,
190
+ * app_id, and collection name, then uses PostgREST on_conflict=id.
191
+ */
192
+ upsert(data: T): Promise<DataRecord<T>>;
186
193
  /** Get a single record by ID (alias for selectById) */
187
194
  get(id: string): Promise<DataRecord<T> | null>;
188
195
  /** Select records with optional filters */
@@ -239,6 +246,8 @@ interface DataTable {
239
246
  interface DataServiceClient {
240
247
  /** Automatically extracted application ID from current URL */
241
248
  readonly appId: string;
249
+ /** User ID decoded from the JWT token when available */
250
+ readonly userId: string | null;
242
251
  /** Access user private data table (user_data) */
243
252
  readonly userData: DataTable;
244
253
  /** Get user data statistics (RPC call) */
package/dist/index.js CHANGED
@@ -44,6 +44,64 @@ var DataServiceError = class extends Error {
44
44
  // src/client.ts
45
45
  var debugToken = null;
46
46
  var manualAppId = null;
47
+ function decodeBase64Url(value) {
48
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
49
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
50
+ const buffer = globalThis.Buffer;
51
+ if (buffer) {
52
+ return buffer.from(padded, "base64").toString("utf8");
53
+ }
54
+ const atobFn = globalThis.atob;
55
+ if (typeof atobFn === "function") {
56
+ const binary = atobFn(padded);
57
+ const bytes = Uint8Array.from(Array.from(binary, (char) => char.charCodeAt(0)));
58
+ const textDecoder = globalThis.TextDecoder;
59
+ if (typeof textDecoder === "function") {
60
+ return new textDecoder().decode(bytes);
61
+ }
62
+ return binary;
63
+ }
64
+ throw new DataServiceError("Unable to decode JWT payload", "JWT_DECODE_UNSUPPORTED");
65
+ }
66
+ function extractUserIdFromToken(token) {
67
+ if (!token) return null;
68
+ const rawToken = token.startsWith("Bearer ") ? token.slice("Bearer ".length) : token;
69
+ const parts = rawToken.split(".");
70
+ if (parts.length < 2) return null;
71
+ try {
72
+ const payload = JSON.parse(decodeBase64Url(parts[1]));
73
+ return payload?.payload?.user_id || payload?.user_id || payload?.sub || null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+ function stableUuidFromParts(...parts) {
79
+ const input = parts.join("");
80
+ let h1 = 3735928559;
81
+ let h2 = 1103547991;
82
+ let h3 = 3235826430;
83
+ let h4 = 305419896;
84
+ for (let i = 0; i < input.length; i++) {
85
+ const ch = input.charCodeAt(i);
86
+ h1 = Math.imul(h1 ^ ch, 2654435761);
87
+ h2 = Math.imul(h2 ^ ch, 1597334677);
88
+ h3 = Math.imul(h3 ^ ch, 2246822507);
89
+ h4 = Math.imul(h4 ^ ch, 3266489909);
90
+ }
91
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
92
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h3 ^ h3 >>> 13, 3266489909);
93
+ h3 = Math.imul(h3 ^ h3 >>> 16, 2246822507) ^ Math.imul(h4 ^ h4 >>> 13, 3266489909);
94
+ h4 = Math.imul(h4 ^ h4 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
95
+ const bytes = new Uint8Array(16);
96
+ new DataView(bytes.buffer).setUint32(0, h1 >>> 0);
97
+ new DataView(bytes.buffer).setUint32(4, h2 >>> 0);
98
+ new DataView(bytes.buffer).setUint32(8, h3 >>> 0);
99
+ new DataView(bytes.buffer).setUint32(12, h4 >>> 0);
100
+ bytes[6] = bytes[6] & 15 | 80;
101
+ bytes[8] = bytes[8] & 63 | 128;
102
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
103
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
104
+ }
47
105
  function debugSetToken(token) {
48
106
  debugToken = token;
49
107
  }
@@ -400,11 +458,12 @@ var QueryBuilderImpl = class {
400
458
  }
401
459
  };
402
460
  var CollectionImpl = class {
403
- constructor(client, tablePath, appId, collectionName) {
461
+ constructor(client, tablePath, appId, collectionName, userId) {
404
462
  this.client = client;
405
463
  this.tablePath = tablePath;
406
464
  this.appId = appId;
407
465
  this.collectionName = collectionName;
466
+ this.userId = userId;
408
467
  }
409
468
  async insert(data, id) {
410
469
  const payload = {
@@ -418,6 +477,27 @@ var CollectionImpl = class {
418
477
  const response = await this.client.post(this.tablePath, payload);
419
478
  return Array.isArray(response) ? response[0] : response;
420
479
  }
480
+ async upsert(data) {
481
+ if (!this.userId) {
482
+ throw new DataServiceError(
483
+ "Unable to derive user_id from token for deterministic upsert",
484
+ "MISSING_USER_ID"
485
+ );
486
+ }
487
+ const id = stableUuidFromParts(this.userId, this.appId, this.collectionName);
488
+ const payload = {
489
+ id,
490
+ app_id: this.appId,
491
+ collection_name: this.collectionName,
492
+ data
493
+ };
494
+ const response = await this.client.post(
495
+ `${this.tablePath}?on_conflict=id`,
496
+ payload,
497
+ { Prefer: "resolution=merge-duplicates,return=representation" }
498
+ );
499
+ return Array.isArray(response) ? response[0] : response;
500
+ }
421
501
  async get(id) {
422
502
  return this.selectById(id);
423
503
  }
@@ -473,13 +553,14 @@ var CollectionImpl = class {
473
553
  }
474
554
  };
475
555
  var DataTableImpl = class {
476
- constructor(client, tablePath, appId) {
556
+ constructor(client, tablePath, appId, userId) {
477
557
  this.client = client;
478
558
  this.tablePath = tablePath;
479
559
  this.appId = appId;
560
+ this.userId = userId;
480
561
  }
481
562
  collection(name) {
482
- return new CollectionImpl(this.client, this.tablePath, this.appId, name);
563
+ return new CollectionImpl(this.client, this.tablePath, this.appId, name, this.userId);
483
564
  }
484
565
  async batchInsert(baseCollectionName, records) {
485
566
  const results = [];
@@ -489,7 +570,8 @@ var DataTableImpl = class {
489
570
  this.client,
490
571
  this.tablePath,
491
572
  this.appId,
492
- uniqueCollectionName
573
+ uniqueCollectionName,
574
+ this.userId
493
575
  );
494
576
  const result = await collection.insert(records[i]);
495
577
  results.push(result);
@@ -504,10 +586,12 @@ var DataServiceClientImpl = class {
504
586
  client;
505
587
  userData;
506
588
  appId;
507
- constructor(client, appId) {
589
+ userId;
590
+ constructor(client, appId, userId) {
508
591
  this.client = client;
509
592
  this.appId = appId;
510
- this.userData = new DataTableImpl(this.client, "/user_data", this.appId);
593
+ this.userId = userId;
594
+ this.userData = new DataTableImpl(this.client, "/user_data", this.appId, userId);
511
595
  }
512
596
  async getStats() {
513
597
  const response = await this.client.post("/rpc/get_user_data_stats");
@@ -561,7 +645,8 @@ async function createClient(config = {}) {
561
645
  }
562
646
  }
563
647
  const appId = manualAppId || parentAppId || extractAppId();
564
- return new DataServiceClientImpl(httpClient, appId);
648
+ const userId = extractUserIdFromToken(token);
649
+ return new DataServiceClientImpl(httpClient, appId, userId);
565
650
  }
566
651
 
567
652
  // src/index.ts
package/dist/index.mjs CHANGED
@@ -13,6 +13,64 @@ var DataServiceError = class extends Error {
13
13
  // src/client.ts
14
14
  var debugToken = null;
15
15
  var manualAppId = null;
16
+ function decodeBase64Url(value) {
17
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
18
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
19
+ const buffer = globalThis.Buffer;
20
+ if (buffer) {
21
+ return buffer.from(padded, "base64").toString("utf8");
22
+ }
23
+ const atobFn = globalThis.atob;
24
+ if (typeof atobFn === "function") {
25
+ const binary = atobFn(padded);
26
+ const bytes = Uint8Array.from(Array.from(binary, (char) => char.charCodeAt(0)));
27
+ const textDecoder = globalThis.TextDecoder;
28
+ if (typeof textDecoder === "function") {
29
+ return new textDecoder().decode(bytes);
30
+ }
31
+ return binary;
32
+ }
33
+ throw new DataServiceError("Unable to decode JWT payload", "JWT_DECODE_UNSUPPORTED");
34
+ }
35
+ function extractUserIdFromToken(token) {
36
+ if (!token) return null;
37
+ const rawToken = token.startsWith("Bearer ") ? token.slice("Bearer ".length) : token;
38
+ const parts = rawToken.split(".");
39
+ if (parts.length < 2) return null;
40
+ try {
41
+ const payload = JSON.parse(decodeBase64Url(parts[1]));
42
+ return payload?.payload?.user_id || payload?.user_id || payload?.sub || null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ function stableUuidFromParts(...parts) {
48
+ const input = parts.join("");
49
+ let h1 = 3735928559;
50
+ let h2 = 1103547991;
51
+ let h3 = 3235826430;
52
+ let h4 = 305419896;
53
+ for (let i = 0; i < input.length; i++) {
54
+ const ch = input.charCodeAt(i);
55
+ h1 = Math.imul(h1 ^ ch, 2654435761);
56
+ h2 = Math.imul(h2 ^ ch, 1597334677);
57
+ h3 = Math.imul(h3 ^ ch, 2246822507);
58
+ h4 = Math.imul(h4 ^ ch, 3266489909);
59
+ }
60
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
61
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h3 ^ h3 >>> 13, 3266489909);
62
+ h3 = Math.imul(h3 ^ h3 >>> 16, 2246822507) ^ Math.imul(h4 ^ h4 >>> 13, 3266489909);
63
+ h4 = Math.imul(h4 ^ h4 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
64
+ const bytes = new Uint8Array(16);
65
+ new DataView(bytes.buffer).setUint32(0, h1 >>> 0);
66
+ new DataView(bytes.buffer).setUint32(4, h2 >>> 0);
67
+ new DataView(bytes.buffer).setUint32(8, h3 >>> 0);
68
+ new DataView(bytes.buffer).setUint32(12, h4 >>> 0);
69
+ bytes[6] = bytes[6] & 15 | 80;
70
+ bytes[8] = bytes[8] & 63 | 128;
71
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
72
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
73
+ }
16
74
  function debugSetToken(token) {
17
75
  debugToken = token;
18
76
  }
@@ -369,11 +427,12 @@ var QueryBuilderImpl = class {
369
427
  }
370
428
  };
371
429
  var CollectionImpl = class {
372
- constructor(client, tablePath, appId, collectionName) {
430
+ constructor(client, tablePath, appId, collectionName, userId) {
373
431
  this.client = client;
374
432
  this.tablePath = tablePath;
375
433
  this.appId = appId;
376
434
  this.collectionName = collectionName;
435
+ this.userId = userId;
377
436
  }
378
437
  async insert(data, id) {
379
438
  const payload = {
@@ -387,6 +446,27 @@ var CollectionImpl = class {
387
446
  const response = await this.client.post(this.tablePath, payload);
388
447
  return Array.isArray(response) ? response[0] : response;
389
448
  }
449
+ async upsert(data) {
450
+ if (!this.userId) {
451
+ throw new DataServiceError(
452
+ "Unable to derive user_id from token for deterministic upsert",
453
+ "MISSING_USER_ID"
454
+ );
455
+ }
456
+ const id = stableUuidFromParts(this.userId, this.appId, this.collectionName);
457
+ const payload = {
458
+ id,
459
+ app_id: this.appId,
460
+ collection_name: this.collectionName,
461
+ data
462
+ };
463
+ const response = await this.client.post(
464
+ `${this.tablePath}?on_conflict=id`,
465
+ payload,
466
+ { Prefer: "resolution=merge-duplicates,return=representation" }
467
+ );
468
+ return Array.isArray(response) ? response[0] : response;
469
+ }
390
470
  async get(id) {
391
471
  return this.selectById(id);
392
472
  }
@@ -442,13 +522,14 @@ var CollectionImpl = class {
442
522
  }
443
523
  };
444
524
  var DataTableImpl = class {
445
- constructor(client, tablePath, appId) {
525
+ constructor(client, tablePath, appId, userId) {
446
526
  this.client = client;
447
527
  this.tablePath = tablePath;
448
528
  this.appId = appId;
529
+ this.userId = userId;
449
530
  }
450
531
  collection(name) {
451
- return new CollectionImpl(this.client, this.tablePath, this.appId, name);
532
+ return new CollectionImpl(this.client, this.tablePath, this.appId, name, this.userId);
452
533
  }
453
534
  async batchInsert(baseCollectionName, records) {
454
535
  const results = [];
@@ -458,7 +539,8 @@ var DataTableImpl = class {
458
539
  this.client,
459
540
  this.tablePath,
460
541
  this.appId,
461
- uniqueCollectionName
542
+ uniqueCollectionName,
543
+ this.userId
462
544
  );
463
545
  const result = await collection.insert(records[i]);
464
546
  results.push(result);
@@ -473,10 +555,12 @@ var DataServiceClientImpl = class {
473
555
  client;
474
556
  userData;
475
557
  appId;
476
- constructor(client, appId) {
558
+ userId;
559
+ constructor(client, appId, userId) {
477
560
  this.client = client;
478
561
  this.appId = appId;
479
- this.userData = new DataTableImpl(this.client, "/user_data", this.appId);
562
+ this.userId = userId;
563
+ this.userData = new DataTableImpl(this.client, "/user_data", this.appId, userId);
480
564
  }
481
565
  async getStats() {
482
566
  const response = await this.client.post("/rpc/get_user_data_stats");
@@ -530,7 +614,8 @@ async function createClient(config = {}) {
530
614
  }
531
615
  }
532
616
  const appId = manualAppId || parentAppId || extractAppId();
533
- return new DataServiceClientImpl(httpClient, appId);
617
+ const userId = extractUserIdFromToken(token);
618
+ return new DataServiceClientImpl(httpClient, appId, userId);
534
619
  }
535
620
 
536
621
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seaverse/dataservice",
3
- "version": "1.8.4",
3
+ "version": "1.8.5",
4
4
  "description": "AI-Friendly Universal Data Storage SDK for TypeScript/JavaScript",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",