import { AkReqResponse, AkReqUploadOptions, AkReq } from '@/uni_modules/ak-req/index.uts' import type { AkReqOptions } from '@/uni_modules/ak-req/index.uts' import { toUniError } from '@/utils/utils.uts' export type AkSupaSignInResult = { access_token : string; refresh_token : string; expires_at : number; user : UTSJSONObject | null; token_type ?: string; expires_in ?: number; raw : UTSJSONObject; } // Count 选项枚举 export type CountOption = 'exact' | 'planned' | 'estimated'; // 定义查询选项类型,兼容 UTS export type AkSupaSelectOptions = { limit ?: number; order ?: string; getcount ?: string; // 保持向后兼容 count ?: CountOption; // 新增:更清晰的 count 选项 head ?: boolean; // 新增:head 模式,只返回元数据 columns ?: string; single ?: boolean; // 新增,支持 single-object rangeFrom ?: number; // 新增:range 分页起始位置 rangeTo ?: number; // 新增:range 分页结束位置 }; // 新增:order方法参数类型 export type OrderOptions = { ascending ?: boolean; }; // 新增类型定义,便于 getSession 返回类型复用 export type AkSupaSessionInfo = { session : AkSupaSignInResult | null; user : UTSJSONObject | null; }; // 链式请求构建器 // 强类型条件定义 type AkSupaCondition = { field : string; // 已经 encodeURIComponent 过 op : string; value : any; logic : string; // 'and' | 'or' }; export class AkSupaQueryBuilder { private _supa : AkSupa; private _table : string; private _filter : UTSJSONObject | null = null; private _options : AkSupaSelectOptions = {}; private _values : UTSJSONObject | Array | null = null; private _single : boolean = false; private _conditions : Array = []; private _nextLogic : string = 'and'; // 新增:记录当前操作类型 private _action : 'select' | 'insert' | 'update' | 'delete' | 'rpc' | null = null; private _orString : string | null = null; // 新增:支持 or 字符串 private _rpcFunction : string | null = null; private _rpcParams : UTSJSONObject | null = null; private _page : number = 1; // 新增:当前页码 constructor(supa : AkSupa, table : string) { this._supa = supa; this._table = table; } // 链式条件方法 eq(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'eq', value); } neq(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'neq', value); } gt(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'gt', value); } gte(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'gte', value); } lt(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'lt', value); } lte(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'lte', value); } 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); } 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, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'not', value); } and() : AkSupaQueryBuilder { this._nextLogic = 'and'; return this; } or(str ?: string) : AkSupaQueryBuilder { if (typeof str == 'string') { this._orString = str; } else { this._nextLogic = 'or'; } return this; } private _addCond(afield : string, op : string, value : any) : AkSupaQueryBuilder { //console.log('add cond:', op, afield, value) const field = encodeURIComponent(afield)!! this._conditions.push({ field, op, value, logic: this._nextLogic }); //console.log(this._conditions) this._nextLogic = 'and'; return this; } // 支持原有 where 方式 where(filter : UTSJSONObject) : AkSupaQueryBuilder { this._filter = filter; return this; } page(page : number) : AkSupaQueryBuilder { this._page = page; // 如果已设置 limit,则自动设置 range let limit = 0; if (typeof this._options.limit == 'number') { limit = this._options.limit ?? 0; } if (limit > 0) { const from = (page - 1) * limit; const to = from + limit - 1; this.range(from, to); } return this; } limit(limit : number) : AkSupaQueryBuilder { this._options.limit = limit; // 总是为 limit 设置对应的 range,确保限制生效 const from = (this._page - 1) * limit; const to = from + limit - 1; this.range(from, to); return this; } order(order : string, options ?: OrderOptions) : AkSupaQueryBuilder { if (options != null && options.ascending == false) { this._options.order = order + '.desc'; } else { this._options.order = order + '.asc'; } return this; } columns(columns : string) : AkSupaQueryBuilder { this._options.columns = columns; return this; } // 新增:专门的 count 方法 count(option : CountOption = 'exact') : AkSupaQueryBuilder { this._options.count = option; this._options.head = true; // count 操作默认使用 head 模式 return this; } // 新增:便捷的 count 方法 countExact() : AkSupaQueryBuilder { return this.count('exact'); } countEstimated() : AkSupaQueryBuilder { return this.count('estimated'); } countPlanned() : AkSupaQueryBuilder { return this.count('planned'); } // 新增:head 模式方法 head(enable : boolean = true) : AkSupaQueryBuilder { this._options.head = enable; return this; } values(values : UTSJSONObject) : AkSupaQueryBuilder { this._values = values; return this; } single() : AkSupaQueryBuilder { this._single = true; return this; } range(from : number, to : number) : AkSupaQueryBuilder { this._options.rangeFrom = from; this._options.rangeTo = to; //console.log('设置 range:', from, 'to', to); return this; } // 将 _conditions 强类型直接转换为 Supabase/PostgREST 查询字符串(不再用 UTSJSONObject 做中转) private _buildFilter() : string | null { if (this._conditions.length == 0 && (this._orString==null || this._orString == "")) { // 兼容 where(filter) 方式 if (this._filter == null) return null; // 兼容旧的 UTSJSONObject filter return buildSupabaseFilterQuery(this._filter); } // 先分组 and/or,全部用 AkSupaCondition 强类型 const ands: AkSupaCondition[] = []; const ors: AkSupaCondition[] = []; for (const c of this._conditions) { if (c.logic == "or") { ors.push(c); } else { ands.push(c); } } const params: string[] = []; // 处理 and 条件 for (const cond of ands) { const k = cond.field; 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(',')})`); } else if (op == 'is' && (val == null || val == 'null')) { params.push(`${k}=is.null`); } else { const opvalstr: string = (typeof val == 'object') ? JSON.stringify(val) : (val as string); params.push(`${k}=${op}.${encodeURIComponent(opvalstr)}`); } } // 处理 or 条件 if (ors.length > 0) { const orStr = ors.map(o => { const k = o.field; 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(",")})`; } if (op == "is" && (val == null)) { return `${k}.is.null`; } return `${k}.${op}.${encodeURIComponent(val as string)}`; }).join(","); params.push(`or=(${orStr})`); } if (this._orString!=null && this._orString !== "") { params.push(`or=(${encodeURIComponent(this._orString!!)})`); } return params.length > 0 ? params.join('&') : null; } select(columns ?: string, opt ?: UTSJSONObject) : AkSupaQueryBuilder { this._action = 'select'; if (columns != null) { this._options.columns = columns; } if (opt != null) { // 合并 opt 到 this._options Object.assign(this._options, opt); } return this; } insert(values : UTSJSONObject | Array) : AkSupaQueryBuilder { this._action = 'insert'; // 检查是否为空 if (Array.isArray(values)) { if (values.length == 0) throw toUniError('No values set for insert', 'Insert操作缺少数据'); } else { if (UTSJSONObject.keys(values).length == 0) throw toUniError('No values set for insert', 'Insert操作缺少数据'); } this._values = values; return this; } update(values : UTSJSONObject) : AkSupaQueryBuilder { this._action = 'update'; //console.log('ak update', this._action) if (UTSJSONObject.keys(values).length == 0) throw toUniError('No values set for update', '更新操作缺少数据'); this._values = values; //console.log('ak update', values) return this; } delete() : AkSupaQueryBuilder { this._action = 'delete'; //console.log('delete action now') const filter = this._buildFilter(); //console.log(filter) if (filter == null) throw toUniError('No filter set for delete', '删除操作缺少筛选条件'); //console.log('delete action') return this; } rpc(functionName : string, params ?: UTSJSONObject) : AkSupaQueryBuilder { this._action = 'rpc'; this._rpcFunction = functionName; this._rpcParams = params; return this; } // 链式请求最终执行方法 - 返回 UTSJSONObject async execute() : Promise> { //console.log('execute') const filter = this._buildFilter(); //console.log('execute', filter) let res : any; switch (this._action) { case 'select': { // 传递 single 状态到 options if (this._single) { this._options.single = true; // 如果是 single 请求,自动设置 limit 为 1 if (this._options.limit == null) { this._options.limit = 1; } //console.log(this._options) } // 保证分页统计 if (this._options.limit != null) { if (this._options.getcount == null && this._options.count == null) { this._options.count = 'exact'; // 优先使用新的 count 选项 } } res = await this._supa.select(this._table, filter, this._options); // 解析 content-range header let total = 0; let hasmore = false; const page = this._page; let resdata = res.data let limit = 0; if (typeof this._options.limit == 'number') { limit = this._options.limit ?? 0; } else if (Array.isArray(resdata)) { limit = resdata.length; } let contentRange : string | null = null; if (res.headers != null) { let theheader = res.headers as UTSJSONObject if (typeof theheader.get == 'function') { contentRange = theheader.get('content-range') as string | null; } else if (typeof theheader['content-range'] == 'string') { contentRange = theheader['content-range'] as string; } } if (contentRange != null) { const match = /\/(\d+)$/.exec(contentRange); if (match != null) { total = parseInt(match[1] ?? "0"); hasmore = (page * limit) < total; } } if (total == 0) { if (typeof res['count'] == 'number') { total = res['count'] as number ?? 0; } else if (Array.isArray(resdata)) { total = resdata.length; } else { total = 0; } } if (!hasmore) hasmore = (page * limit) < total; // 如果是 head 模式,只返回 count 信息 if (this._options.head == true) { return { data: null, // head 模式不返回数据 total, page, limit, hasmore: false, // head 模式不需要分页信息 origin: res, status: res.status, headers: res.headers, error: res.error } as AkReqResponse; } return { data: res.data, total, page, limit, hasmore, origin: res, status: res.status, headers: res.headers, error: res.error } as AkReqResponse; } case 'insert': { const insertValues = this._values; if (insertValues == null) throw toUniError('No values set for insert', '插入操作缺少数据'); res = await this._supa.insert(this._table, insertValues); break; } case 'update': { const updateValues = this._values; if (updateValues == null) throw toUniError('No values set for update', '更新操作缺少数据'); if (filter == null) throw toUniError('No filter set for update', '更新操作缺少筛选条件'); // Update操作只支持单个对象,不支持数组 if (Array.isArray(updateValues)) throw toUniError('Update does not support array values', '更新操作不支持数组数据'); res = await this._supa.update(this._table, filter, updateValues as UTSJSONObject); break; } case 'delete': { if (filter == null) throw toUniError('No filter set for delete', '删除操作缺少筛选条件'); res = await this._supa.delete(this._table, filter); break; } case 'rpc': { if (this._rpcFunction == null) throw toUniError('No RPC function specified', 'RPC调用缺少函数名'); res = await this._supa.rpc(this._rpcFunction as string, this._rpcParams); break; } default: { res = await this._supa.select(this._table, filter, this._options); } } // 保证 data 字段存在(不能赋null,赋空对象或空字符串) if (res["data"] == null) res["data"] = {}; return res; } // 新增:支持类型转换的执行方法 async executeAs() : Promise>> { 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; } // 尝试类型转换 let convertedData : T | Array | null = null; try { if (Array.isArray(result.data)) { // 处理数组数据 const dataArray = result.data; const convertedArray : Array = []; //console.log(convertedArray) 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(); // //console.log('ak parsed') // #endif // #ifndef APP-ANDROID const parsed = item as T; // #endif if (parsed != null) { convertedArray.push(parsed); } else { console.warn('转换失败,使用原始对象:', item); convertedArray.push(item as T); } } else { // 将普通对象转换为 UTSJSONObject 后再 parse const jsonObj = new UTSJSONObject(item); // #ifdef APP-ANDROID const parsed = jsonObj.parse(); // #endif // #ifndef APP-ANDROID const parsed = jsonObj as T; // #endif if (parsed != null) { convertedArray.push(parsed); } else { console.warn('转换失败,使用原始对象:', item); convertedArray.push(item as T); } } } convertedData = convertedArray; } else { // 处理单个对象 const convertedArray : Array = []; if (result.data instanceof UTSJSONObject) { const parsed = result.data.parse(); if (parsed != null) { convertedArray.push(parsed); } else { //console.log('转换失败:', result.data) } } else { const jsonObj = new UTSJSONObject(result.data); const parsed = jsonObj.parse(); if (parsed != null) { convertedArray.push(parsed); } else { //console.log('转换失败:', result.data) } } convertedData = convertedArray; } } catch (e) { console.warn('数据类型转换失败,使用原始数据:', e); console.log(result.data) // 转换失败时,使用原始数据 convertedData = result.data as T | Array; } result.data = convertedData const aaa = result as AkReqResponse // const aaa = { // status: result.status, // data: convertedData, // headers: result.headers, // error: result.error, // total: result.total, // page: result.page, // limit: result.limit, // hasmore: result.hasmore, // origin: result.origin // } return aaa; } } // 新增:链式 Storage 上传 export class AkSupaStorageUploadBuilder { private _supa : AkSupa; private _bucket : string = ''; private _path : string = ''; private _file : string = ''; private _options : UTSJSONObject = {}; constructor(supa : AkSupa, bucket : string) { this._supa = supa; this._bucket = bucket; } path(path : string) : AkSupaStorageUploadBuilder { this._path = path; return this; } file(file : string) : AkSupaStorageUploadBuilder { this._file = file; return this; } options(options : UTSJSONObject) : AkSupaStorageUploadBuilder { this._options = options; return this; } async upload() : Promise> { if (this._bucket == '' || this._path == '' || this._file == '') { throw toUniError('bucket, path, file are required', '上传文件缺少必要参数'); } const url = `${this._supa.baseUrl}/storage/v1/object/${this._bucket}/${this._path}`; const apikey = this._supa.apikey; // 适配 uni.uploadFile const uploadOptions : AkReqUploadOptions = { url, filePath: this._file, // 这里假设 file 是本地路径 name: 'file', // 默认字段名 headers: {}, apikey, formData: this._options }; return await AkReq.upload(uploadOptions); } } // 新增:明确的 StorageBucket 类,支持链式 upload class AkSupaStorageBucket { private supa : AkSupa; private bucket : string; constructor(supa : AkSupa, bucket : string) { this.supa = supa; this.bucket = bucket; } async upload(path : string, filePath : string, options ?: UTSJSONObject) : Promise> { const url = `${this.supa.baseUrl}/storage/v1/object/${this.bucket}/${path}`; let headers : UTSJSONObject = { apikey: this.supa.apikey }; const formData : UTSJSONObject = {}; if (options != null && typeof options == 'object') { if (typeof options.get == 'function' && options.get('x-upsert') != null) { headers['x-upsert'] = options.get('x-upsert'); } const keys = UTSJSONObject.keys(options); for (let i = 0; i < keys.length; i++) { const k = keys[i]; if (k != 'x-upsert') formData[k] = options.get(k); } } const token = AkReq.getToken(); if (token != null && !(token == '')) { headers['Authorization'] = `Bearer ${token}`; } return await AkReq.upload({ url, filePath, name: 'file', apikey: this.supa.apikey, formData, headers }); } } export class AkSupaStorageApi { private _supa : AkSupa; constructor(supa : AkSupa) { this._supa = supa; } from(bucket : string) : AkSupaStorageBucket { return new AkSupaStorageBucket(this._supa, bucket); } } export class AkSupa { baseUrl : string; apikey : string; session : AkSupaSignInResult | null = null; user : UTSJSONObject | null = null; storage : AkSupaStorageApi; constructor(baseUrl : string, apikey : string) { this.baseUrl = baseUrl; this.apikey = apikey; this.storage = new AkSupaStorageApi(this); } async resetPassword(email : string) : Promise { const res = await AkReq.request({ url: this.baseUrl + '/auth/v1/recover', method: 'POST', headers: { apikey: this.apikey, 'Content-Type': 'application/json' } as UTSJSONObject, data: { email } as UTSJSONObject, contentType: 'application/json' }, false); // Supabase returns 200 when the reset email is sent successfully return res.status == 200; } async signOut() { this.session = null this.user = null } async signIn(email : string, password : string) : Promise { 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, contentType: 'application/json' }, false); //console.log(res) const data = new UTSJSONObject(res.data); // 修正:确保data为UTSJSONObject const access_token = data.getString('access_token') ?? ''; const refresh_token = data.getString('refresh_token') ?? ''; const expires_at = data.getNumber('expires_at') ?? 0; const user = data.getJSON('user'); //console.log(user, data) AkReq.setToken(access_token, refresh_token, expires_at); const session : AkSupaSignInResult = { access_token: access_token, refresh_token: refresh_token, expires_at: expires_at, user: user, token_type: data.getString('token_type') ?? '', expires_in: data.getNumber('expires_in') ?? 0, raw: data }; this.session = session; this.user = user; //console.log(this.user) return session; } /** * 获取当前 session 和 user */ getSession() : AkSupaSessionInfo { return { session: this.session, user: this.user }; } async signUp(email : string, password : string) : Promise { const res = await AkReq.request({ url: this.baseUrl + '/auth/v1/signup', method: 'POST', headers: { apikey: this.apikey, 'Content-Type': 'application/json' } as UTSJSONObject, data: { email, password } as UTSJSONObject, contentType: 'application/json' }, false); return res.data as UTSJSONObject; } /** * 查询表数据(GET方式,支持多条件、limit等,filter自动转为supabase风格query) * filter 支持: * { usr_id: { lt: 800 }, name: { ilike: '%foo%' }, status: 'active', age: { gte: 18, lte: 30 } } * 操作符支持 eq, neq, lt, lte, gt, gte, like, ilike, in, is, not, contains, containedBy, range, fts, plfts, phfts, wfts */ async select(table : string, filter ?: string | null, options ?: AkSupaSelectOptions) : Promise> { let url = this.baseUrl + '/rest/v1/' + table; let headers = { apikey: this.apikey, 'Content-Type': 'application/json', Authorization: `Bearer ${AkReq.getToken() ?? ''}` } as UTSJSONObject; let params : string[] = []; if (options != null) { if (options.columns != null && !(options.columns == "")) params.push('select=' + encodeURIComponent(options.columns ?? "")); if (options.limit != null) { params.push('limit=' + options.limit); //console.log('设置 limit 参数:', options.limit); } if (options.order != null && !(options.order == "")) params.push('order=' + encodeURIComponent(options.order ?? "")); if (options.rangeFrom != null && options.rangeTo != null) { headers['Range'] = `${options.rangeFrom}-${options.rangeTo}`; headers['Range-Unit'] = 'items'; //console.log('设置 Range 头部:', `${options.rangeFrom}-${options.rangeTo}`); } // 向后兼容:支持旧的 getcount 参数 let countOption = options.count ?? options.getcount; if (countOption != null) { headers['Prefer'] = `count=${countOption}`; } // 新增:head 模式支持 if (options.head == true) { //console.log('使用 head 模式,只返回元数据'); // HEAD 请求用于只获取 count,不返回数据 if (headers['Prefer'] != null) { headers['Prefer'] = (headers['Prefer'] as string) + ',return=minimal'; } else { headers['Prefer'] = 'return=minimal'; } } if (options.single == true) { //console.log('使用 single() 模式'); if (headers['Prefer'] != null) { headers['Prefer'] = (headers['Prefer'] as string) + ',return=representation,single-object'; } else { headers['Prefer'] = 'return=representation,single-object'; } } } else { params.push('select=*'); } // 直接用 string filter if (filter!=null && filter !== "") { params.push(filter!!); } if (params.length > 0) { url += '?' + params.join('&'); } //console.log(url) // 确定HTTP方法:如果是head模式,使用HEAD方法 let httpMethod = 'GET'; if (options != null && options.head == true) { httpMethod = 'HEAD'; //console.log('使用 HEAD 方法进行 count 查询'); } let reqOptions : AkReqOptions = { url, method: httpMethod, headers }; return await this.requestWithAutoRefresh(reqOptions); } async select_uts(table : string, filter ?: UTSJSONObject | null, options ?: AkSupaSelectOptions) : Promise> { const filter_str = buildSupabaseFilterQuery(filter) return this.select(table,filter_str,options) } /** * 插入表数据 * @param table 表名 * @param row 插入对象 * @returns 插入结果 */ async insert(table : string, row : UTSJSONObject | Array) : Promise> { const url = this.baseUrl + '/rest/v1/' + table; const headers = { apikey: this.apikey, 'Content-Type': 'application/json', Authorization: `Bearer ${AkReq.getToken() ?? ''}`, Prefer: 'return=representation' } as UTSJSONObject; // 如果是数组,直接传递;如果是单个对象,也直接传递 // Supabase REST API 原生支持两种格式 let reqOptions : AkReqOptions = { url, method: 'POST', headers, data: row, // 可以是单个对象或数组 contentType: 'application/json' }; return await this.requestWithAutoRefresh(reqOptions); } /** * 更新表数据 * @param table 表名 * @param filter 过滤条件对象 * @param values 更新内容对象 * @returns 更新结果 */ async update(table : string, filter : string | null, values : UTSJSONObject) : Promise> { let url = this.baseUrl + '/rest/v1/' + table; if (filter!=null && filter !== "") { url += '?' + filter; } const headers = { apikey: this.apikey, 'Content-Type': 'application/json', Authorization: `Bearer ${AkReq.getToken() ?? ''}`, Prefer: 'return=representation' } as UTSJSONObject; let reqOptions : AkReqOptions = { url, method: 'PATCH', headers, data: values, contentType: 'application/json' }; return await this.requestWithAutoRefresh(reqOptions); } /** * 删除表数据 * @param table 表名 * @param filter 过滤条件对象 * @returns 删除结果 */ async delete(table : string, filter : string | null) : Promise> { let url = this.baseUrl + '/rest/v1/' + table; if (filter!=null && filter !== "") { url += '?' + filter; } const headers = { apikey: this.apikey, 'Content-Type': 'application/json', Authorization: `Bearer ${AkReq.getToken() ?? ''}`, Prefer: 'return=representation' } as UTSJSONObject; let reqOptions : AkReqOptions = { url, method: 'DELETE', headers, contentType: 'application/json' }; return await this.requestWithAutoRefresh(reqOptions); } /** * 调用 Supabase/PostgREST RPC (function) * @param functionName 函数名 * @param params 参数对象 * @returns AkReqResponse */ async rpc(functionName : string, params ?: UTSJSONObject) : Promise> { const url = `${this.baseUrl}/rest/v1/rpc/${functionName}`; const headers = { apikey: this.apikey, 'Content-Type': 'application/json', Authorization: `Bearer ${AkReq.getToken() ?? ''}` } as UTSJSONObject; let reqOptions : AkReqOptions = { url, method: 'POST', headers, data: params ?? {}, contentType: 'application/json' }; return await this.requestWithAutoRefresh(reqOptions); } /** * 兼容 supabase-js 风格 * @param tableName 表名 */ from(tableName : string) : AkSupaQueryBuilder { return new AkSupaQueryBuilder(this, tableName); } // AkSupa类内新增:自动刷新session async refreshSession() : Promise { if (this.session == null || this.session?.refresh_token == null) return false; try { 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, contentType: 'application/json' }, false); if (res.status == 200 && (res.data != null)) { const data = res.data as UTSJSONObject; const access_token = data.getString('access_token') ?? ''; const refresh_token = data.getString('refresh_token') ?? ''; const expires_at = data.getNumber('expires_at') ?? 0; const user = data.getJSON('user'); this.session = { access_token, refresh_token, expires_at, user, token_type: data.getString('token_type') ?? '', expires_in: data.getNumber('expires_in') ?? 0, raw: data }; this.user = user; // 更新本地token AkReq.setToken(access_token, refresh_token, expires_at); return true; } return false; } catch (e) { return false; } } // AkSupa类内新增:自动刷新封装 async requestWithAutoRefresh(reqOptions : AkReqOptions, isRetry = false) : Promise> { let res = await AkReq.request(reqOptions, false); // JWT过期:Supabase风格 const isJwtExpired = (res.status == 401); //res != null && res.data != null && typeof res.data == 'object' && (res.data as UTSJSONObject)?.getString('code') == 'PGRST301'; // 401未授权 const isUnauthorized = (res.status == 401); if ((isJwtExpired || isUnauthorized) && !isRetry) { const ok = await this.refreshSession(); if (ok) { let headers = reqOptions.headers if (headers == null) { headers = new UTSJSONObject() } if (typeof headers.set == 'function') { headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`) reqOptions.headers = headers } res = await AkReq.request(reqOptions, false); } else { uni.removeStorageSync('user_id'); uni.removeStorageSync('token'); uni.reLaunch({ url: '/pages/user/login' }); throw toUniError('登录已过期,请重新登录', '用户认证失败'); } } return res; } } // 工具函数:将 UTSJSONObject filter 转为 Supabase/PostgREST 查询字符串 function buildSupabaseFilterQuery(filter : UTSJSONObject | null) : string { //console.log(filter) if (filter == null) return ""; // 类型保护:如果不是 UTSJSONObject,自动包裹 if (typeof filter.get !== 'function') { try { filter = new UTSJSONObject(filter as any) } catch (e) { console.warn('filter 不是 UTSJSONObject,且无法转换', filter) return '' } } const params : string[] = []; const keys : string[] = UTSJSONObject.keys(filter); for (let i = 0; i < keys.length; i++) { const k = keys[i]; const v = filter.get(k); if (k == 'or' && typeof v == 'string') { params.push(`or=(${v})`); continue; } if (v != null && typeof v == 'object' && typeof (v as UTSJSONObject).get == 'function') { const vObj = v as UTSJSONObject; const opKeys = UTSJSONObject.keys(vObj); for (let j = 0; j < opKeys.length; j++) { const op = opKeys[j]; const opVal = vObj.get(op); if ((op == 'in' || op == 'not.in') && Array.isArray(opVal)) { params.push(`${k}=${op}.(${opVal.map(x => typeof x == 'object' ? encodeURIComponent(JSON.stringify(x)) : encodeURIComponent(x.toString())).join(',')})`); } else if (op == 'is' && (opVal == null || opVal == 'null')) { params.push(`${k}=is.null`); } else { const opvalstr : string = (typeof opVal == 'object') ? JSON.stringify(opVal) : (opVal as string); params.push(`${k}=${op}.${encodeURIComponent(opvalstr)}`); } } } else if (v != null && typeof v == 'object') { const vObj = v as UTSJSONObject; const opKeys = UTSJSONObject.keys(vObj); for (let j = 0; j < opKeys.length; j++) { const op = opKeys[j]; const opVal = vObj.get(op); params.push(`${k}=${op}.${encodeURIComponent(!(opVal == null) ? (typeof opVal == 'object' ? JSON.stringify(opVal) : opVal.toString()) : '')}`); } } else { params.push(`${k}=eq.${encodeURIComponent(!(v == null) ? v.toString() : '')}`); } } return params.join('&'); } export default AkSupa;