1024 lines
32 KiB
Plaintext
1024 lines
32 KiB
Plaintext
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; |