@seaverse/data-sdk 0.1.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 +408 -0
- package/dist/index.cjs +784 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +542 -0
- package/dist/index.js +770 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var axios = require('axios');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Type definitions for SeaVerse Data SDK
|
|
7
|
+
*
|
|
8
|
+
* 基于 PostgreSQL/AlloyDB + PostgREST + RLS 的数据类型定义
|
|
9
|
+
*/
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Error Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* SDK Error class
|
|
15
|
+
*/
|
|
16
|
+
class SDKError extends Error {
|
|
17
|
+
constructor(message, code, statusCode, details) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.statusCode = statusCode;
|
|
21
|
+
this.details = details;
|
|
22
|
+
this.name = 'SDKError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* SDK Error base class (legacy alias)
|
|
27
|
+
*/
|
|
28
|
+
class DataSDKError extends SDKError {
|
|
29
|
+
constructor(message, code, statusCode) {
|
|
30
|
+
super(message, code || 'SDK_ERROR', statusCode);
|
|
31
|
+
this.name = 'DataSDKError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Permission denied error (403)
|
|
36
|
+
*/
|
|
37
|
+
class PermissionError extends SDKError {
|
|
38
|
+
constructor(message = 'Permission denied') {
|
|
39
|
+
super(message, 'PERMISSION_DENIED', 403);
|
|
40
|
+
this.name = 'PermissionError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resource not found error (404)
|
|
45
|
+
*/
|
|
46
|
+
class NotFoundError extends SDKError {
|
|
47
|
+
constructor(message = 'Resource not found') {
|
|
48
|
+
super(message, 'NOT_FOUND', 404);
|
|
49
|
+
this.name = 'NotFoundError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Rate limit exceeded error (429)
|
|
54
|
+
*/
|
|
55
|
+
class RateLimitError extends SDKError {
|
|
56
|
+
constructor(message = 'Rate limit exceeded') {
|
|
57
|
+
super(message, 'RATE_LIMIT_EXCEEDED', 429);
|
|
58
|
+
this.name = 'RateLimitError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validation error (400)
|
|
63
|
+
*/
|
|
64
|
+
class ValidationError extends SDKError {
|
|
65
|
+
constructor(message) {
|
|
66
|
+
super(message, 'VALIDATION_ERROR', 400);
|
|
67
|
+
this.name = 'ValidationError';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Environment Configuration for Data SDK
|
|
73
|
+
* 环境配置模块 - 支持多环境部署
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* 预定义的环境配置
|
|
77
|
+
*
|
|
78
|
+
* 使用说明:
|
|
79
|
+
* - production: 正式生产环境
|
|
80
|
+
* - staging: 预发布环境
|
|
81
|
+
* - development: 开发测试环境
|
|
82
|
+
* - local: 本地开发环境
|
|
83
|
+
*/
|
|
84
|
+
const ENVIRONMENT_CONFIGS = {
|
|
85
|
+
production: {
|
|
86
|
+
name: 'production',
|
|
87
|
+
baseURL: 'https://data.seaverse.ai',
|
|
88
|
+
isProduction: true,
|
|
89
|
+
},
|
|
90
|
+
staging: {
|
|
91
|
+
name: 'staging',
|
|
92
|
+
baseURL: 'https://data.sg.seaverse.dev',
|
|
93
|
+
isProduction: false,
|
|
94
|
+
},
|
|
95
|
+
development: {
|
|
96
|
+
name: 'development',
|
|
97
|
+
baseURL: 'https://data-dev.seaverse.dev',
|
|
98
|
+
isProduction: false,
|
|
99
|
+
},
|
|
100
|
+
local: {
|
|
101
|
+
name: 'local',
|
|
102
|
+
baseURL: 'http://localhost:3000',
|
|
103
|
+
isProduction: false,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* 根据当前运行环境自动检测应该使用的环境配置
|
|
108
|
+
*
|
|
109
|
+
* 检测规则:
|
|
110
|
+
* 1. localhost/127.0.0.1 → local
|
|
111
|
+
* 2. 包含 -dev 或 .dev → development
|
|
112
|
+
* 3. 包含 staging → staging
|
|
113
|
+
* 4. 其他 → production (默认)
|
|
114
|
+
*
|
|
115
|
+
* @returns 检测到的环境类型
|
|
116
|
+
*/
|
|
117
|
+
function detectEnvironment() {
|
|
118
|
+
// Node.js 环境或非浏览器环境,默认返回 staging(安全起见)
|
|
119
|
+
if (typeof window === 'undefined' || typeof window.location === 'undefined') {
|
|
120
|
+
return 'staging';
|
|
121
|
+
}
|
|
122
|
+
const hostname = window.location.hostname.toLowerCase();
|
|
123
|
+
// 本地开发环境
|
|
124
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0') {
|
|
125
|
+
return 'local';
|
|
126
|
+
}
|
|
127
|
+
// 开发环境 (包含 -dev 或 .dev)
|
|
128
|
+
if (hostname.includes('-dev.') || hostname.includes('.dev') || hostname.endsWith('.dev')) {
|
|
129
|
+
return 'development';
|
|
130
|
+
}
|
|
131
|
+
// Staging 环境
|
|
132
|
+
if (hostname.includes('-staging.') || hostname.includes('staging.')) {
|
|
133
|
+
return 'staging';
|
|
134
|
+
}
|
|
135
|
+
// 默认使用 production
|
|
136
|
+
return 'production';
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 获取环境配置
|
|
140
|
+
*
|
|
141
|
+
* @param env 环境类型,如果不提供则自动检测
|
|
142
|
+
* @returns 环境配置对象
|
|
143
|
+
*/
|
|
144
|
+
function getEnvironmentConfig(env) {
|
|
145
|
+
const targetEnv = env || detectEnvironment();
|
|
146
|
+
return ENVIRONMENT_CONFIGS[targetEnv];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 解析用户配置,返回最终的 baseURL
|
|
150
|
+
*
|
|
151
|
+
* 优先级:
|
|
152
|
+
* 1. 显式传入的 baseURL(最高优先级)
|
|
153
|
+
* 2. environment 参数指定的环境
|
|
154
|
+
* 3. 自动检测环境(最低优先级)
|
|
155
|
+
*
|
|
156
|
+
* @param options 用户配置选项
|
|
157
|
+
* @returns 最终使用的 baseURL
|
|
158
|
+
*/
|
|
159
|
+
function resolveBaseURL(options) {
|
|
160
|
+
// 优先级 1: 显式传入的 baseURL
|
|
161
|
+
if (options.baseURL) {
|
|
162
|
+
return options.baseURL;
|
|
163
|
+
}
|
|
164
|
+
// 优先级 2: environment 参数
|
|
165
|
+
if (options.environment) {
|
|
166
|
+
return ENVIRONMENT_CONFIGS[options.environment].baseURL;
|
|
167
|
+
}
|
|
168
|
+
// 优先级 3: 自动检测
|
|
169
|
+
const detectedEnv = detectEnvironment();
|
|
170
|
+
return ENVIRONMENT_CONFIGS[detectedEnv].baseURL;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 默认超时时间(毫秒)
|
|
174
|
+
*/
|
|
175
|
+
const DEFAULT_TIMEOUT = 10000; // 10 seconds
|
|
176
|
+
/**
|
|
177
|
+
* API 端点路径
|
|
178
|
+
*/
|
|
179
|
+
const ENDPOINTS = {
|
|
180
|
+
// Data operations (PostgREST)
|
|
181
|
+
APP_DATA: '/app_data',
|
|
182
|
+
// Admin roles
|
|
183
|
+
APP_ADMINS: '/app_admins',
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Data SDK Client
|
|
188
|
+
*
|
|
189
|
+
* 基于 PostgreSQL/AlloyDB + PostgREST + RLS 的数据 SDK
|
|
190
|
+
*/
|
|
191
|
+
/**
|
|
192
|
+
* SeaVerse Data Client
|
|
193
|
+
*
|
|
194
|
+
* 高性能数据服务SDK,基于PostgREST + RLS架构
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```typescript
|
|
198
|
+
* const client = new DataClient({
|
|
199
|
+
* appId: 'my-app-id',
|
|
200
|
+
* token: 'user-jwt-token',
|
|
201
|
+
* });
|
|
202
|
+
*
|
|
203
|
+
* // Query data
|
|
204
|
+
* const notes = await client.query({
|
|
205
|
+
* table: 'notes',
|
|
206
|
+
* filters: { category: 'work' },
|
|
207
|
+
* order: { field: 'created_at', direction: 'desc' },
|
|
208
|
+
* });
|
|
209
|
+
*
|
|
210
|
+
* // Create data
|
|
211
|
+
* const note = await client.create({
|
|
212
|
+
* table: 'notes',
|
|
213
|
+
* data: { title: 'My Note', content: '...' },
|
|
214
|
+
* visibility: 'private',
|
|
215
|
+
* });
|
|
216
|
+
*
|
|
217
|
+
* // Update data
|
|
218
|
+
* await client.update('note-id', {
|
|
219
|
+
* data: { title: 'Updated' },
|
|
220
|
+
* });
|
|
221
|
+
*
|
|
222
|
+
* // Delete data
|
|
223
|
+
* await client.delete('note-id');
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
class DataClient {
|
|
227
|
+
constructor(options) {
|
|
228
|
+
const { timeout = DEFAULT_TIMEOUT, headers = {}, appId, token, debug = false, } = options;
|
|
229
|
+
if (!appId) {
|
|
230
|
+
throw new ValidationError('appId is required');
|
|
231
|
+
}
|
|
232
|
+
if (!token) {
|
|
233
|
+
throw new ValidationError('token is required');
|
|
234
|
+
}
|
|
235
|
+
this.appId = appId;
|
|
236
|
+
this.token = token;
|
|
237
|
+
this.debug = debug;
|
|
238
|
+
// 解析 baseURL(支持环境自动检测)
|
|
239
|
+
const baseURL = resolveBaseURL({
|
|
240
|
+
baseURL: options.baseURL,
|
|
241
|
+
environment: options.environment,
|
|
242
|
+
});
|
|
243
|
+
this.axiosInstance = axios.create({
|
|
244
|
+
baseURL,
|
|
245
|
+
timeout,
|
|
246
|
+
headers: {
|
|
247
|
+
'Content-Type': 'application/json',
|
|
248
|
+
'Authorization': `Bearer ${token}`,
|
|
249
|
+
...headers,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
// Add response interceptor for error handling
|
|
253
|
+
this.axiosInstance.interceptors.response.use((response) => response, (error) => {
|
|
254
|
+
throw this._handleError(error);
|
|
255
|
+
});
|
|
256
|
+
this.log('SDK initialized', { apiUrl: baseURL, appId });
|
|
257
|
+
}
|
|
258
|
+
// ============================================
|
|
259
|
+
// Logging Methods
|
|
260
|
+
// ============================================
|
|
261
|
+
log(message, data) {
|
|
262
|
+
if (this.debug) {
|
|
263
|
+
console.log(`[DataSDK] ${message}`, data || '');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
logError(message, error) {
|
|
267
|
+
console.error(`[DataSDK] ${message}`, error);
|
|
268
|
+
}
|
|
269
|
+
// ============================================
|
|
270
|
+
// Helper Methods
|
|
271
|
+
// ============================================
|
|
272
|
+
/**
|
|
273
|
+
* Handle axios errors and convert to SDK errors
|
|
274
|
+
* @private
|
|
275
|
+
*/
|
|
276
|
+
_handleError(error) {
|
|
277
|
+
const status = error.response?.status;
|
|
278
|
+
const message = error.response?.data?.message || error.message;
|
|
279
|
+
switch (status) {
|
|
280
|
+
case 403:
|
|
281
|
+
return new PermissionError(message);
|
|
282
|
+
case 404:
|
|
283
|
+
return new NotFoundError(message);
|
|
284
|
+
case 429:
|
|
285
|
+
return new RateLimitError(message);
|
|
286
|
+
case 400:
|
|
287
|
+
return new ValidationError(message);
|
|
288
|
+
default:
|
|
289
|
+
return new SDKError(message, 'REQUEST_FAILED', status);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Get user ID from JWT token
|
|
294
|
+
* @private
|
|
295
|
+
*/
|
|
296
|
+
getUserIdFromToken() {
|
|
297
|
+
try {
|
|
298
|
+
// JWT token format: header.payload.signature
|
|
299
|
+
const parts = this.token.split('.');
|
|
300
|
+
if (parts.length !== 3) {
|
|
301
|
+
throw new Error('Invalid JWT token');
|
|
302
|
+
}
|
|
303
|
+
// Decode payload (base64url)
|
|
304
|
+
const payload = parts[1];
|
|
305
|
+
// Cross-platform base64 decoding
|
|
306
|
+
let decodedString;
|
|
307
|
+
const globalObj = globalThis;
|
|
308
|
+
if (typeof globalObj.atob === 'function') {
|
|
309
|
+
// Browser environment
|
|
310
|
+
decodedString = globalObj.atob(payload);
|
|
311
|
+
}
|
|
312
|
+
else if (typeof globalObj.Buffer !== 'undefined') {
|
|
313
|
+
// Node.js environment
|
|
314
|
+
decodedString = globalObj.Buffer.from(payload, 'base64').toString('utf-8');
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// Fallback: manual base64 decode
|
|
318
|
+
decodedString = this.base64Decode(payload);
|
|
319
|
+
}
|
|
320
|
+
const claims = JSON.parse(decodedString);
|
|
321
|
+
return claims.sub;
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
throw new SDKError('Unable to parse JWT token', 'INVALID_TOKEN', 401, error);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Base64 decode fallback
|
|
329
|
+
* @private
|
|
330
|
+
*/
|
|
331
|
+
base64Decode(str) {
|
|
332
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
|
333
|
+
let output = '';
|
|
334
|
+
str = String(str).replace(/=+$/, '');
|
|
335
|
+
if (str.length % 4 === 1) {
|
|
336
|
+
throw new Error('Invalid base64 string');
|
|
337
|
+
}
|
|
338
|
+
for (let bc = 0, bs = 0, buffer, i = 0; (buffer = str.charAt(i++)); ~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4)
|
|
339
|
+
? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
|
|
340
|
+
: 0) {
|
|
341
|
+
buffer = chars.indexOf(buffer);
|
|
342
|
+
}
|
|
343
|
+
return output;
|
|
344
|
+
}
|
|
345
|
+
// ============================================
|
|
346
|
+
// Data Operation Methods
|
|
347
|
+
// ============================================
|
|
348
|
+
/**
|
|
349
|
+
* Query data list
|
|
350
|
+
*
|
|
351
|
+
* 查询数据列表,支持过滤、排序、分页
|
|
352
|
+
*
|
|
353
|
+
* @param options - Query options
|
|
354
|
+
* @returns Array of data records
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* ```typescript
|
|
358
|
+
* const notes = await client.query({
|
|
359
|
+
* table: 'notes',
|
|
360
|
+
* filters: {
|
|
361
|
+
* category: 'work',
|
|
362
|
+
* visibility: 'private',
|
|
363
|
+
* },
|
|
364
|
+
* order: {
|
|
365
|
+
* field: 'created_at',
|
|
366
|
+
* direction: 'desc',
|
|
367
|
+
* },
|
|
368
|
+
* pagination: {
|
|
369
|
+
* limit: 20,
|
|
370
|
+
* offset: 0,
|
|
371
|
+
* },
|
|
372
|
+
* });
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
async query(options) {
|
|
376
|
+
const query = {
|
|
377
|
+
app_id: `eq.${this.appId}`,
|
|
378
|
+
table_name: `eq.${options.table}`,
|
|
379
|
+
};
|
|
380
|
+
// Add filters
|
|
381
|
+
if (options.filters) {
|
|
382
|
+
const { filters } = options;
|
|
383
|
+
if (filters.id) {
|
|
384
|
+
query.id = `eq.${filters.id}`;
|
|
385
|
+
}
|
|
386
|
+
if (filters.category) {
|
|
387
|
+
query.category = `eq.${filters.category}`;
|
|
388
|
+
}
|
|
389
|
+
if (filters.visibility) {
|
|
390
|
+
query.visibility = `eq.${filters.visibility}`;
|
|
391
|
+
}
|
|
392
|
+
if (filters.tags && filters.tags.length > 0) {
|
|
393
|
+
// PostgREST array contains operator
|
|
394
|
+
query.tags = `cs.{${filters.tags.join(',')}}`;
|
|
395
|
+
}
|
|
396
|
+
if (filters.created_after) {
|
|
397
|
+
query.created_at = `gte.${filters.created_after}`;
|
|
398
|
+
}
|
|
399
|
+
if (filters.created_before) {
|
|
400
|
+
if (query.created_at) {
|
|
401
|
+
// If created_at already exists, use 'and' to combine
|
|
402
|
+
query.and = `(created_at.gte.${filters.created_after},created_at.lt.${filters.created_before})`;
|
|
403
|
+
delete query.created_at;
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
query.created_at = `lt.${filters.created_before}`;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// JSONB field filters (data_value)
|
|
410
|
+
if (filters.data) {
|
|
411
|
+
Object.entries(filters.data).forEach(([key, value]) => {
|
|
412
|
+
// PostgREST JSONB query syntax: data_value->>key (text extraction)
|
|
413
|
+
query[`data_value->>${key}`] = `eq.${value}`;
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Add sorting
|
|
418
|
+
if (options.order) {
|
|
419
|
+
const direction = options.order.direction === 'asc' ? '' : '.desc';
|
|
420
|
+
query.order = `${options.order.field}${direction}`;
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
// Default sort by created_at desc
|
|
424
|
+
query.order = 'created_at.desc';
|
|
425
|
+
}
|
|
426
|
+
// Add pagination
|
|
427
|
+
if (options.pagination) {
|
|
428
|
+
query.limit = options.pagination.limit.toString();
|
|
429
|
+
query.offset = options.pagination.offset.toString();
|
|
430
|
+
}
|
|
431
|
+
this.log('Querying data', query);
|
|
432
|
+
const response = await this.axiosInstance.get(ENDPOINTS.APP_DATA, { params: query });
|
|
433
|
+
return Array.isArray(response.data) ? response.data : [response.data];
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Get single data record by ID
|
|
437
|
+
*
|
|
438
|
+
* 通过 ID 查询单条数据
|
|
439
|
+
*
|
|
440
|
+
* @param id - Data ID
|
|
441
|
+
* @returns Data record or null if not found
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* ```typescript
|
|
445
|
+
* const note = await client.get('note-id-123');
|
|
446
|
+
* console.log(note?.data_value.title);
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
async get(id) {
|
|
450
|
+
try {
|
|
451
|
+
const response = await this.axiosInstance.get(ENDPOINTS.APP_DATA, {
|
|
452
|
+
params: {
|
|
453
|
+
id: `eq.${id}`,
|
|
454
|
+
},
|
|
455
|
+
headers: {
|
|
456
|
+
'Accept': 'application/vnd.pgrst.object+json', // 返回单个对象
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
const results = Array.isArray(response.data) ? response.data : [response.data];
|
|
460
|
+
return results && results.length > 0 ? results[0] : null;
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
this._handleError(error);
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Create data
|
|
469
|
+
*
|
|
470
|
+
* 创建新数据
|
|
471
|
+
*
|
|
472
|
+
* @param options - Create options
|
|
473
|
+
* @returns Created data record
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* ```typescript
|
|
477
|
+
* const newNote = await client.create({
|
|
478
|
+
* table: 'notes',
|
|
479
|
+
* data: {
|
|
480
|
+
* title: 'My Note',
|
|
481
|
+
* content: 'Note content...',
|
|
482
|
+
* },
|
|
483
|
+
* visibility: 'private',
|
|
484
|
+
* category: 'work',
|
|
485
|
+
* tags: ['important', 'todo'],
|
|
486
|
+
* });
|
|
487
|
+
* ```
|
|
488
|
+
*/
|
|
489
|
+
async create(options) {
|
|
490
|
+
const body = {
|
|
491
|
+
app_id: this.appId,
|
|
492
|
+
table_name: options.table,
|
|
493
|
+
data_value: options.data,
|
|
494
|
+
owner_user_id: this.getUserIdFromToken(),
|
|
495
|
+
visibility: options.visibility || 'private',
|
|
496
|
+
category: options.category,
|
|
497
|
+
tags: options.tags,
|
|
498
|
+
};
|
|
499
|
+
this.log('Creating data', body);
|
|
500
|
+
const response = await this.axiosInstance.post(ENDPOINTS.APP_DATA, body, {
|
|
501
|
+
headers: {
|
|
502
|
+
'Prefer': 'return=representation', // Return created record
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
const results = Array.isArray(response.data) ? response.data : [response.data];
|
|
506
|
+
return results[0];
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Update data
|
|
510
|
+
*
|
|
511
|
+
* 更新数据
|
|
512
|
+
*
|
|
513
|
+
* @param id - Data ID
|
|
514
|
+
* @param options - Update options
|
|
515
|
+
* @returns Updated data record
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* ```typescript
|
|
519
|
+
* const updated = await client.update('note-id-123', {
|
|
520
|
+
* data: {
|
|
521
|
+
* title: 'Updated Title',
|
|
522
|
+
* content: 'Updated content',
|
|
523
|
+
* },
|
|
524
|
+
* category: 'personal',
|
|
525
|
+
* });
|
|
526
|
+
* ```
|
|
527
|
+
*/
|
|
528
|
+
async update(id, options) {
|
|
529
|
+
const body = {
|
|
530
|
+
updated_at: new Date().toISOString(),
|
|
531
|
+
};
|
|
532
|
+
if (options.data !== undefined) {
|
|
533
|
+
body.data_value = options.data;
|
|
534
|
+
}
|
|
535
|
+
if (options.visibility !== undefined) {
|
|
536
|
+
body.visibility = options.visibility;
|
|
537
|
+
}
|
|
538
|
+
if (options.category !== undefined) {
|
|
539
|
+
body.category = options.category;
|
|
540
|
+
}
|
|
541
|
+
if (options.tags !== undefined) {
|
|
542
|
+
body.tags = options.tags;
|
|
543
|
+
}
|
|
544
|
+
this.log('Updating data', { id, body });
|
|
545
|
+
const response = await this.axiosInstance.patch(ENDPOINTS.APP_DATA, body, {
|
|
546
|
+
params: {
|
|
547
|
+
id: `eq.${id}`,
|
|
548
|
+
},
|
|
549
|
+
headers: {
|
|
550
|
+
'Prefer': 'return=representation',
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
const results = Array.isArray(response.data) ? response.data : [response.data];
|
|
554
|
+
if (!results || results.length === 0) {
|
|
555
|
+
throw new NotFoundError('Data not found or no permission to update');
|
|
556
|
+
}
|
|
557
|
+
return results[0];
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Delete data
|
|
561
|
+
*
|
|
562
|
+
* 删除数据
|
|
563
|
+
*
|
|
564
|
+
* @param id - Data ID
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* ```typescript
|
|
568
|
+
* await client.delete('note-id-123');
|
|
569
|
+
* ```
|
|
570
|
+
*/
|
|
571
|
+
async delete(id) {
|
|
572
|
+
this.log('Deleting data', { id });
|
|
573
|
+
await this.axiosInstance.delete(ENDPOINTS.APP_DATA, {
|
|
574
|
+
params: {
|
|
575
|
+
app_id: `eq.${this.appId}`,
|
|
576
|
+
id: `eq.${id}`,
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Batch delete data
|
|
582
|
+
*
|
|
583
|
+
* 批量删除数据
|
|
584
|
+
*
|
|
585
|
+
* @param ids - Array of data IDs
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* ```typescript
|
|
589
|
+
* await client.batchDelete(['id-1', 'id-2', 'id-3']);
|
|
590
|
+
* ```
|
|
591
|
+
*/
|
|
592
|
+
async batchDelete(ids) {
|
|
593
|
+
if (ids.length === 0)
|
|
594
|
+
return;
|
|
595
|
+
this.log('Batch deleting data', { ids });
|
|
596
|
+
await this.axiosInstance.delete(ENDPOINTS.APP_DATA, {
|
|
597
|
+
params: {
|
|
598
|
+
app_id: `eq.${this.appId}`,
|
|
599
|
+
id: `in.(${ids.join(',')})`,
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Query with pagination metadata
|
|
605
|
+
*
|
|
606
|
+
* 查询数据并返回分页信息
|
|
607
|
+
*
|
|
608
|
+
* @param options - Query options
|
|
609
|
+
* @returns Paginated response with metadata
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```typescript
|
|
613
|
+
* const result = await client.queryWithPagination({
|
|
614
|
+
* table: 'posts',
|
|
615
|
+
* pagination: { limit: 10, offset: 0 },
|
|
616
|
+
* });
|
|
617
|
+
*
|
|
618
|
+
* console.log(`Total: ${result.total}, Has more: ${result.hasMore}`);
|
|
619
|
+
* ```
|
|
620
|
+
*/
|
|
621
|
+
async queryWithPagination(options) {
|
|
622
|
+
const limit = options.pagination?.limit || 10;
|
|
623
|
+
const offset = options.pagination?.offset || 0;
|
|
624
|
+
const query = {
|
|
625
|
+
app_id: `eq.${this.appId}`,
|
|
626
|
+
table_name: `eq.${options.table}`,
|
|
627
|
+
limit: limit.toString(),
|
|
628
|
+
offset: offset.toString(),
|
|
629
|
+
};
|
|
630
|
+
// Add filters (same as query method)
|
|
631
|
+
if (options.filters) {
|
|
632
|
+
const { filters } = options;
|
|
633
|
+
if (filters.id)
|
|
634
|
+
query.id = `eq.${filters.id}`;
|
|
635
|
+
if (filters.category)
|
|
636
|
+
query.category = `eq.${filters.category}`;
|
|
637
|
+
if (filters.visibility)
|
|
638
|
+
query.visibility = `eq.${filters.visibility}`;
|
|
639
|
+
if (filters.tags && filters.tags.length > 0) {
|
|
640
|
+
query.tags = `cs.{${filters.tags.join(',')}}`;
|
|
641
|
+
}
|
|
642
|
+
if (filters.created_after) {
|
|
643
|
+
query.created_at = `gte.${filters.created_after}`;
|
|
644
|
+
}
|
|
645
|
+
if (filters.created_before) {
|
|
646
|
+
if (query.created_at) {
|
|
647
|
+
query.and = `(created_at.gte.${filters.created_after},created_at.lt.${filters.created_before})`;
|
|
648
|
+
delete query.created_at;
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
query.created_at = `lt.${filters.created_before}`;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (filters.data) {
|
|
655
|
+
Object.entries(filters.data).forEach(([key, value]) => {
|
|
656
|
+
query[`data_value->>${key}`] = `eq.${value}`;
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// Add sorting
|
|
661
|
+
if (options.order) {
|
|
662
|
+
const direction = options.order.direction === 'asc' ? '' : '.desc';
|
|
663
|
+
query.order = `${options.order.field}${direction}`;
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
query.order = 'created_at.desc';
|
|
667
|
+
}
|
|
668
|
+
this.log('Querying with pagination', query);
|
|
669
|
+
// Request with count
|
|
670
|
+
const response = await this.axiosInstance.get(ENDPOINTS.APP_DATA, {
|
|
671
|
+
params: query,
|
|
672
|
+
headers: {
|
|
673
|
+
'Prefer': 'count=exact', // Request total count
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
const data = Array.isArray(response.data) ? response.data : [response.data];
|
|
677
|
+
// Extract total count from Content-Range header
|
|
678
|
+
// Format: "0-9/100" means items 0-9, total 100
|
|
679
|
+
let total;
|
|
680
|
+
let hasMore;
|
|
681
|
+
const contentRange = response.headers['content-range'];
|
|
682
|
+
if (contentRange) {
|
|
683
|
+
const match = contentRange.match(/\/(\d+|\*)/);
|
|
684
|
+
if (match && match[1] !== '*') {
|
|
685
|
+
total = parseInt(match[1], 10);
|
|
686
|
+
hasMore = offset + data.length < total;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
data,
|
|
691
|
+
total,
|
|
692
|
+
limit,
|
|
693
|
+
offset,
|
|
694
|
+
hasMore,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
// ============================================
|
|
698
|
+
// Admin Operation Methods
|
|
699
|
+
// ============================================
|
|
700
|
+
/**
|
|
701
|
+
* Check if current user is admin
|
|
702
|
+
*
|
|
703
|
+
* 检查当前用户是否是管理员
|
|
704
|
+
*
|
|
705
|
+
* @returns True if user is admin
|
|
706
|
+
*
|
|
707
|
+
* @example
|
|
708
|
+
* ```typescript
|
|
709
|
+
* const isAdmin = await client.isAdmin();
|
|
710
|
+
* if (isAdmin) {
|
|
711
|
+
* // User has admin privileges
|
|
712
|
+
* }
|
|
713
|
+
* ```
|
|
714
|
+
*/
|
|
715
|
+
async isAdmin() {
|
|
716
|
+
try {
|
|
717
|
+
const response = await this.axiosInstance.get(ENDPOINTS.APP_ADMINS, {
|
|
718
|
+
params: {
|
|
719
|
+
app_id: `eq.${this.appId}`,
|
|
720
|
+
user_id: `eq.${this.getUserIdFromToken()}`,
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
const roles = Array.isArray(response.data) ? response.data : [response.data];
|
|
724
|
+
return roles.length > 0;
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
this.logError('Check admin permission failed', error);
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// ============================================
|
|
732
|
+
// Configuration Methods
|
|
733
|
+
// ============================================
|
|
734
|
+
/**
|
|
735
|
+
* Update token
|
|
736
|
+
*
|
|
737
|
+
* 更新 JWT token
|
|
738
|
+
*
|
|
739
|
+
* @param token - New JWT token
|
|
740
|
+
*/
|
|
741
|
+
updateToken(token) {
|
|
742
|
+
this.token = token;
|
|
743
|
+
this.axiosInstance.defaults.headers['Authorization'] = `Bearer ${token}`;
|
|
744
|
+
this.log('Token updated');
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Update app ID
|
|
748
|
+
*
|
|
749
|
+
* 更新应用 ID
|
|
750
|
+
*
|
|
751
|
+
* @param appId - New application ID
|
|
752
|
+
*/
|
|
753
|
+
updateAppId(appId) {
|
|
754
|
+
this.appId = appId;
|
|
755
|
+
this.log('AppId updated', { appId });
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Get current app ID
|
|
759
|
+
*/
|
|
760
|
+
getAppId() {
|
|
761
|
+
return this.appId;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Get current user ID
|
|
765
|
+
*/
|
|
766
|
+
getUserId() {
|
|
767
|
+
return this.getUserIdFromToken();
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
|
|
772
|
+
exports.DataClient = DataClient;
|
|
773
|
+
exports.DataSDKError = DataSDKError;
|
|
774
|
+
exports.ENDPOINTS = ENDPOINTS;
|
|
775
|
+
exports.ENVIRONMENT_CONFIGS = ENVIRONMENT_CONFIGS;
|
|
776
|
+
exports.NotFoundError = NotFoundError;
|
|
777
|
+
exports.PermissionError = PermissionError;
|
|
778
|
+
exports.RateLimitError = RateLimitError;
|
|
779
|
+
exports.SDKError = SDKError;
|
|
780
|
+
exports.ValidationError = ValidationError;
|
|
781
|
+
exports.detectEnvironment = detectEnvironment;
|
|
782
|
+
exports.getEnvironmentConfig = getEnvironmentConfig;
|
|
783
|
+
exports.resolveBaseURL = resolveBaseURL;
|
|
784
|
+
//# sourceMappingURL=index.cjs.map
|