Files
akmon/components/supadb/aksupa.uts
2026-01-20 08:04:15 +08:00

1024 lines
32 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<UTSJSONObject> | null = null;
private _single : boolean = false;
private _conditions : Array<AkSupaCondition> = [];
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<UTSJSONObject>) : 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<AkReqResponse<any>> {
//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<any>;
}
return {
data: res.data,
total,
page,
limit,
hasmore,
origin: res,
status: res.status,
headers: res.headers,
error: res.error
} as AkReqResponse<any>;
}
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<T = any>() : Promise<AkReqResponse<T | Array<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;
}
// 尝试类型转换
let convertedData : T | Array<T> | null = null;
try {
if (Array.isArray(result.data)) {
// 处理数组数据
const dataArray = result.data;
const convertedArray : Array<T> = [];
//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<T>();
// //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<T>();
// #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<T> = [];
if (result.data instanceof UTSJSONObject) {
const parsed = result.data.parse<T>();
if (parsed != null) {
convertedArray.push(parsed);
}
else {
//console.log('转换失败:', result.data)
}
} else {
const jsonObj = new UTSJSONObject(result.data);
const parsed = jsonObj.parse<T>();
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<T>;
}
result.data = convertedData
const aaa = result as AkReqResponse<T | Array<T>
// 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<AkReqResponse<any>> {
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<AkReqResponse<any>> {
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<boolean> {
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<AkSupaSignInResult> {
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<UTSJSONObject> {
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<AkReqResponse<any>> {
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<AkReqResponse<any>> {
const filter_str = buildSupabaseFilterQuery(filter)
return this.select(table,filter_str,options)
}
/**
* 插入表数据
* @param table 表名
* @param row 插入对象
* @returns 插入结果
*/
async insert(table : string, row : UTSJSONObject | Array<UTSJSONObject>) : Promise<AkReqResponse<any>> {
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<AkReqResponse<any>> {
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<AkReqResponse<any>> {
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<any>
*/
async rpc(functionName : string, params ?: UTSJSONObject) : Promise<AkReqResponse<any>> {
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<boolean> {
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<AkReqResponse<any>> {
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;