Merge remote-tracking branch 'origin/cyh666666/consumer'
This commit is contained in:
@@ -81,11 +81,11 @@ export class AkSupaQueryBuilder {
|
||||
like(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'like', value); }
|
||||
ilike(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'ilike', value); }
|
||||
in(field : string, value : any[]) : AkSupaQueryBuilder { return this._addCond(field, 'in', value); }
|
||||
is(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'is', value); }
|
||||
is(field : string, value : any | null) : AkSupaQueryBuilder { return this._addCond(field, 'is', value); }
|
||||
contains(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cs', value); }
|
||||
containedBy(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cd', value); }
|
||||
not(field : string, opOrValue : any, value?: any) : AkSupaQueryBuilder {
|
||||
if (value !== undefined) {
|
||||
not(field : string, opOrValue : any, value: any | null = null) : AkSupaQueryBuilder {
|
||||
if (value != null) {
|
||||
// 三元形式:field, operator, value
|
||||
// 例如 not('badge', 'is', null) -> badge=not.is.null
|
||||
const combinedOp = 'not.' + opOrValue;
|
||||
@@ -115,15 +115,31 @@ export class AkSupaQueryBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
private _addCond(afield : string, op : string, value : any) : AkSupaQueryBuilder {
|
||||
private _addCond(afield : string, op : string, value : any | null) : AkSupaQueryBuilder {
|
||||
//console.log('add cond:', op, afield, value)
|
||||
const field = encodeURIComponent(afield)!!
|
||||
// 将 null 转换为字符串 'null',避免构造对象时缺少 value 属性
|
||||
let safeValue = value;
|
||||
// 将值安全存储,避免安卓端类型转换问题
|
||||
let safeValue: any | null = value;
|
||||
if (value === null) {
|
||||
safeValue = 'null';
|
||||
} else if (Array.isArray(value)) {
|
||||
// 数组类型保持原样,用于 in 操作符
|
||||
safeValue = value;
|
||||
} else if (typeof value === 'number') {
|
||||
// 数字类型保持原样
|
||||
safeValue = value;
|
||||
} else if (typeof value === 'boolean') {
|
||||
// 布尔类型保持原样
|
||||
safeValue = value;
|
||||
} else if (typeof value !== 'string') {
|
||||
// 其他类型尝试转换为字符串
|
||||
try {
|
||||
safeValue = value.toString();
|
||||
} catch (e) {
|
||||
safeValue = '';
|
||||
}
|
||||
}
|
||||
this._conditions.push({ field, op, value: safeValue, logic: this._nextLogic });
|
||||
this._conditions.push({ field, op, value: safeValue ?? '', logic: this._nextLogic });
|
||||
//console.log(this._conditions)
|
||||
this._nextLogic = 'and';
|
||||
return this;
|
||||
@@ -211,6 +227,23 @@ export class AkSupaQueryBuilder {
|
||||
//console.log('设置 range:', from, 'to', to);
|
||||
return this;
|
||||
}
|
||||
|
||||
// 辅助函数:安全地将值转换为字符串
|
||||
private _valToStr(val: any): string {
|
||||
if (val == null) return '';
|
||||
try {
|
||||
// 尝试直接调用 toString
|
||||
return val.toString();
|
||||
} catch (e) {
|
||||
try {
|
||||
// 尝试 JSON 序列化
|
||||
return JSON.stringify(val);
|
||||
} catch (e2) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将 _conditions 强类型直接转换为 Supabase/PostgREST 查询字符串(不再用 UTSJSONObject 做中转)
|
||||
private _buildFilter() : string | null {
|
||||
if (this._conditions.length == 0 && (this._orString==null || this._orString == "")) {
|
||||
@@ -238,12 +271,13 @@ export class AkSupaQueryBuilder {
|
||||
const op = cond.op;
|
||||
const val = cond.value;
|
||||
if ((op == 'in' || op == 'not.in') && Array.isArray(val)) {
|
||||
params.push(`${k}=${op}.(${val.map(x => typeof x == 'object' ? encodeURIComponent(JSON.stringify(x)) : encodeURIComponent(x.toString())).join(',')})`);
|
||||
params.push(`${k}=${op}.(${val.map(x => this._valToStr(x)).map(x => encodeURIComponent(x)).join(',')})`);
|
||||
} else if ((op == 'is' || op == 'not.is') && (val == null || val == 'null')) {
|
||||
params.push(`${k}=${op}.null`);
|
||||
} else if (op == 'like' || op == 'ilike') {
|
||||
params.push(`${k}=${op}.${this._valToStr(val)}`);
|
||||
} else {
|
||||
const opvalstr: string = (typeof val == 'object') ? JSON.stringify(val) : (val as string);
|
||||
params.push(`${k}=${op}.${encodeURIComponent(opvalstr)}`);
|
||||
params.push(`${k}=${op}.${encodeURIComponent(this._valToStr(val))}`);
|
||||
}
|
||||
}
|
||||
// 处理 or 条件
|
||||
@@ -253,22 +287,26 @@ export class AkSupaQueryBuilder {
|
||||
const op = o.op;
|
||||
const val = o.value;
|
||||
if (op == "in" && Array.isArray(val)) {
|
||||
return `${k}.in.(${val.map(x => encodeURIComponent(x as string)).join(",")})`;
|
||||
return `${k}.in.(${val.map(x => encodeURIComponent(this._valToStr(x))).join(",")})`;
|
||||
}
|
||||
if (op == "is" && (val == null)) {
|
||||
return `${k}.is.null`;
|
||||
}
|
||||
return `${k}.${op}.${encodeURIComponent(val as string)}`;
|
||||
if (op == "like" || op == "ilike") {
|
||||
return `${k}.${op}.${this._valToStr(val)}`;
|
||||
}
|
||||
return `${k}.${op}.${encodeURIComponent(this._valToStr(val))}`;
|
||||
}).join(",");
|
||||
params.push(`or=(${orStr})`);
|
||||
}
|
||||
if (this._orString!=null && this._orString !== "") {
|
||||
params.push(`or=(${encodeURIComponent(this._orString!!)})`);
|
||||
console.log('[AkSupaQueryBuilder] or字符串:', this._orString)
|
||||
params.push(`or=(${this._orString!!})`);
|
||||
}
|
||||
return params.length > 0 ? params.join('&') : null;
|
||||
}
|
||||
|
||||
select(columns ?: string, opt ?: UTSJSONObject) : AkSupaQueryBuilder {
|
||||
select(columns : string = "*", opt : UTSJSONObject | null = null) : AkSupaQueryBuilder {
|
||||
this._action = 'select';
|
||||
if (columns != null) {
|
||||
this._options.columns = columns;
|
||||
@@ -318,7 +356,7 @@ export class AkSupaQueryBuilder {
|
||||
async execute() : Promise<AkReqResponse<any>> {
|
||||
//console.log('execute')
|
||||
const filter = this._buildFilter();
|
||||
//console.log('execute', filter)
|
||||
console.log('[AkSupaQueryBuilder] execute - 表:', this._table, 'filter:', filter)
|
||||
let res : any;
|
||||
switch (this._action) {
|
||||
case 'select': {
|
||||
@@ -366,12 +404,23 @@ export class AkSupaQueryBuilder {
|
||||
}
|
||||
}
|
||||
if (total == 0) {
|
||||
if (typeof res['count'] == 'number') {
|
||||
total = res['count'] as number ?? 0;
|
||||
// 使用 JSON 序列化访问 res 对象
|
||||
const resStr = JSON.stringify(res)
|
||||
const resParsed = JSON.parse(resStr)
|
||||
if (resParsed != null) {
|
||||
const resObj = resParsed as UTSJSONObject
|
||||
const countVal = resObj.getNumber('count')
|
||||
if (countVal != null) {
|
||||
total = countVal
|
||||
} else if (Array.isArray(resdata)) {
|
||||
total = resdata.length
|
||||
} else {
|
||||
total = 0
|
||||
}
|
||||
} else if (Array.isArray(resdata)) {
|
||||
total = resdata.length;
|
||||
total = resdata.length
|
||||
} else {
|
||||
total = 0;
|
||||
total = 0
|
||||
}
|
||||
}
|
||||
if (!hasmore) hasmore = (page * limit) < total; // 如果是 head 模式,只返回 count 信息
|
||||
@@ -433,41 +482,24 @@ export class AkSupaQueryBuilder {
|
||||
if (res["data"] == null) res["data"] = {};
|
||||
return res;
|
||||
} // 新增:支持类型转换的执行方法
|
||||
async executeAs<T = any>() : Promise<AkReqResponse<T | Array<T>>> {
|
||||
async executeAs<T>() : Promise<AkReqResponse<T>> {
|
||||
const result = await this.execute();
|
||||
|
||||
// 如果原始 data 是 null,直接返回 null
|
||||
if (result.data == null) {
|
||||
const aaa = {
|
||||
status: result.status,
|
||||
data: null,
|
||||
headers: result.headers,
|
||||
error: result.error,
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
hasmore: result.hasmore,
|
||||
origin: result.origin
|
||||
}
|
||||
return aaa;
|
||||
return result as AkReqResponse<T>;
|
||||
}
|
||||
|
||||
// 尝试类型转换
|
||||
let convertedData : T | Array<T> | null = null;
|
||||
let convertedData : any | null = null;
|
||||
|
||||
try {
|
||||
if (Array.isArray(result.data)) {
|
||||
// 处理数组数据
|
||||
const dataArray = result.data;
|
||||
const convertedArray : Array<T> = [];
|
||||
//console.log(convertedArray)
|
||||
const convertedArray : Array<any> = [];
|
||||
for (let i = 0; i < dataArray.length; i++) {
|
||||
const item = dataArray[i];
|
||||
if (item instanceof UTSJSONObject) {
|
||||
// #ifdef APP-ANDROID
|
||||
// //console.log(item)
|
||||
const parsed = item.parse<T>();
|
||||
// //console.log('ak parsed')
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID
|
||||
const parsed = item as T;
|
||||
@@ -476,10 +508,9 @@ export class AkSupaQueryBuilder {
|
||||
convertedArray.push(parsed);
|
||||
} else {
|
||||
console.warn('转换失败,使用原始对象:', item);
|
||||
convertedArray.push(item as T);
|
||||
convertedArray.push(item);
|
||||
}
|
||||
} else {
|
||||
// 将普通对象转换为 UTSJSONObject 后再 parse
|
||||
const jsonObj = new UTSJSONObject(item);
|
||||
// #ifdef APP-ANDROID
|
||||
const parsed = jsonObj.parse<T>();
|
||||
@@ -492,15 +523,14 @@ export class AkSupaQueryBuilder {
|
||||
}
|
||||
else {
|
||||
console.warn('转换失败,使用原始对象:', item);
|
||||
convertedArray.push(item as T);
|
||||
convertedArray.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
convertedData = convertedArray;
|
||||
|
||||
} else {
|
||||
// 处理单个对象
|
||||
const convertedArray : Array<T> = [];
|
||||
const convertedArray : Array<any> = [];
|
||||
if (result.data instanceof UTSJSONObject) {
|
||||
const parsed = result.data.parse<T>();
|
||||
|
||||
@@ -525,8 +555,7 @@ export class AkSupaQueryBuilder {
|
||||
} catch (e) {
|
||||
console.warn('数据类型转换失败,使用原始数据:', e);
|
||||
console.log(result.data)
|
||||
// 转换失败时,使用原始数据
|
||||
convertedData = result.data as T | Array<T>;
|
||||
convertedData = result.data as any;
|
||||
}
|
||||
result.data = convertedData
|
||||
const aaa = result as AkReqResponse<T | Array<T>>
|
||||
@@ -543,6 +572,8 @@ export class AkSupaQueryBuilder {
|
||||
// }
|
||||
return aaa;
|
||||
|
||||
//cyh
|
||||
//return result as AkReqResponse<T>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,7 +792,7 @@ export class AkSupa {
|
||||
token_type: 'bearer',
|
||||
expires_in: 0,
|
||||
raw: user
|
||||
} as any;
|
||||
} as AkSupaSignInResult;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -793,14 +824,17 @@ export class AkSupa {
|
||||
if (this.apikey == null || this.apikey.trim() === '' || this.apikey === 'your-anon-key') {
|
||||
throw new Error('Supabase 配置错误:请在 ak/config.uts 中设置 SUPA_KEY(当前为占位符)');
|
||||
}
|
||||
const headers = new UTSJSONObject()
|
||||
headers.set('apikey', this.apikey)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
const reqData = new UTSJSONObject()
|
||||
reqData.set('email', email)
|
||||
reqData.set('password', password)
|
||||
const res = await AkReq.request({
|
||||
url: this.baseUrl + '/auth/v1/token?grant_type=password',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: this.apikey,
|
||||
'Content-Type': 'application/json'
|
||||
} as UTSJSONObject,
|
||||
data: { email, password } as UTSJSONObject,
|
||||
headers: headers,
|
||||
data: reqData,
|
||||
contentType: 'application/json'
|
||||
}, false);
|
||||
// 如果响应不是 2xx(例如 401),提取后端错误信息并抛出,便于上层显示具体原因
|
||||
@@ -810,7 +844,14 @@ export class AkSupa {
|
||||
try {
|
||||
if (res.data != null) {
|
||||
const obj = new UTSJSONObject(res.data);
|
||||
msg = obj.getString('message') ?? obj.getString('error') ?? obj.getString('msg') ?? obj.getString('description') ?? obj.getString('error_description') ?? msg;
|
||||
const rawMsg = obj.getString('message') ?? obj.getString('error') ?? obj.getString('msg') ?? obj.getString('description') ?? obj.getString('error_description') ?? '';
|
||||
|
||||
// 核心修复:在这里拦截英文错误并转换为中文
|
||||
if (rawMsg.includes('Invalid login credentials')) {
|
||||
msg = '用户名或密码错误';
|
||||
} else if (rawMsg != '') {
|
||||
msg = rawMsg;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
@@ -1094,7 +1135,7 @@ async delete(table : string, filter : string | null) : Promise<AkReqResponse<any
|
||||
url,
|
||||
method: 'POST',
|
||||
headers,
|
||||
data: params ?? {},
|
||||
data: params ?? new UTSJSONObject(),
|
||||
contentType: 'application/json'
|
||||
};
|
||||
return await this.requestWithAutoRefresh(reqOptions);
|
||||
@@ -1106,18 +1147,36 @@ async delete(table : string, filter : string | null) : Promise<AkReqResponse<any
|
||||
from(tableName : string) : AkSupaQueryBuilder {
|
||||
return new AkSupaQueryBuilder(this, tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建实时订阅通道 (兼容 Supabase Realtime 接口,目前使用轮询模拟)
|
||||
* @param topic 通道名称,如 public:table
|
||||
*/
|
||||
channel(topic: string): AkSupaRealtimeChannel {
|
||||
return new AkSupaRealtimeChannel(this, topic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除通道
|
||||
*/
|
||||
removeChannel(channel: AkSupaRealtimeChannel): Promise<string> {
|
||||
channel.unsubscribe();
|
||||
return Promise.resolve('ok');
|
||||
}
|
||||
// AkSupa类内新增:自动刷新session
|
||||
async refreshSession() : Promise<boolean> {
|
||||
if (this.session == null || this.session?.refresh_token == null) return false;
|
||||
try {
|
||||
const headers = new UTSJSONObject()
|
||||
headers.set('apikey', this.apikey)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
const data = new UTSJSONObject()
|
||||
data.set('refresh_token', this.session?.refresh_token)
|
||||
const res = await AkReq.request({
|
||||
url: this.baseUrl + '/auth/v1/token?grant_type=refresh_token',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: this.apikey,
|
||||
'Content-Type': 'application/json'
|
||||
} as UTSJSONObject,
|
||||
data: { refresh_token: this.session?.refresh_token } as UTSJSONObject,
|
||||
headers: headers,
|
||||
data: data,
|
||||
contentType: 'application/json'
|
||||
}, true);
|
||||
if (res.status == 200 && (res.data != null)) {
|
||||
@@ -1146,6 +1205,23 @@ async delete(table : string, filter : string | null) : Promise<AkReqResponse<any
|
||||
}
|
||||
}
|
||||
|
||||
async updateUserMetadata(metadata: UTSJSONObject): Promise<UTSJSONObject> {
|
||||
const headers = new UTSJSONObject()
|
||||
headers.set('apikey', this.apikey)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`)
|
||||
const data = new UTSJSONObject()
|
||||
data.set('data', metadata)
|
||||
const res = await AkReq.request({
|
||||
url: this.baseUrl + '/auth/v1/user',
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
data: data,
|
||||
contentType: 'application/json'
|
||||
}, false);
|
||||
return res.data as UTSJSONObject;
|
||||
}
|
||||
|
||||
// AkSupa类内新增:自动刷新封装
|
||||
async requestWithAutoRefresh(reqOptions : AkReqOptions, isRetry = false) : Promise<AkReqResponse<any>> {
|
||||
let res = await AkReq.request(reqOptions, false);
|
||||
|
||||
Reference in New Issue
Block a user