@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 +3 -0
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +92 -7
- package/dist/index.mjs +92 -7
- package/package.json +1 -1
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
|
-
|
|
589
|
+
userId;
|
|
590
|
+
constructor(client, appId, userId) {
|
|
508
591
|
this.client = client;
|
|
509
592
|
this.appId = appId;
|
|
510
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
558
|
+
userId;
|
|
559
|
+
constructor(client, appId, userId) {
|
|
477
560
|
this.client = client;
|
|
478
561
|
this.appId = appId;
|
|
479
|
-
this.
|
|
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
|
-
|
|
617
|
+
const userId = extractUserIdFromToken(token);
|
|
618
|
+
return new DataServiceClientImpl(httpClient, appId, userId);
|
|
534
619
|
}
|
|
535
620
|
|
|
536
621
|
// src/index.ts
|