Files
medical-mall/components/supadb/aksupa.uts

1321 lines
41 KiB
Plaintext
Raw 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 | 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 | null = null) : AkSupaQueryBuilder {
if (value != null) {
// 三元形式field, operator, value
// 例如 not('badge', 'is', null) -> badge=not.is.null
const combinedOp = 'not.' + opOrValue;
// 将 null 转换为字符串 'null',避免构造对象时缺少 value 属性
let safeValue = value;
if (value === null) {
safeValue = 'null';
}
return this._addCond(field, combinedOp, safeValue);
} else {
// 二元形式field, value
let safeValue = opOrValue;
if (opOrValue === null) {
safeValue = 'null';
}
return this._addCond(field, 'not', safeValue);
}
}
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 | null) : AkSupaQueryBuilder {
//console.log('add cond:', op, afield, value)
const field = encodeURIComponent(afield)!!
// 将值安全存储,避免安卓端类型转换问题
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 });
//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;
}
// 辅助函数:安全地将值转换为字符串
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 == "")) {
// 兼容 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 => 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 {
params.push(`${k}=${op}.${encodeURIComponent(this._valToStr(val))}`);
}
}
// 处理 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(this._valToStr(x))).join(",")})`;
}
if (op == "is" && (val == null)) {
return `${k}.is.null`;
}
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 !== "") {
console.log('[AkSupaQueryBuilder] or字符串:', this._orString)
params.push(`or=(${this._orString!!})`);
}
return params.length > 0 ? params.join('&') : null;
}
select(columns : string = "*", opt : UTSJSONObject | null = null) : 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('[AkSupaQueryBuilder] execute - 表:', this._table, 'filter:', 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) {
// 使用 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
} 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>() : Promise<AkReqResponse<T>> {
const result = await this.execute();
if (result.data == null) {
return result as AkReqResponse<T>;
}
let convertedData : any | null = null;
try {
if (Array.isArray(result.data)) {
const dataArray = result.data;
const convertedArray : Array<any> = [];
for (let i = 0; i < dataArray.length; i++) {
const item = dataArray[i];
if (item instanceof UTSJSONObject) {
// #ifdef APP-ANDROID
const parsed = item.parse<T>();
// #endif
// #ifndef APP-ANDROID
const parsed = item as T;
// #endif
if (parsed != null) {
convertedArray.push(parsed);
} else {
console.warn('转换失败,使用原始对象:', item);
convertedArray.push(item);
}
} else {
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);
}
}
}
convertedData = convertedArray;
} else {
const convertedArray : Array<any> = [];
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 any;
}
result.data = convertedData
return result as AkReqResponse<T>;
}
}
// 新增:链式 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);
// [CHANGE][2026-01-30] hydrate user/session from persisted token (see docs: components/supadb/docs/CHANGELOG.md)
try {
this.hydrateSessionFromStorage();
} catch (e) {
// ignore
}
}
// [CHANGE][2026-01-30] hydrate user from /auth/v1/user when token exists in storage
async hydrateSessionFromStorage() : Promise<boolean> {
try {
const token = AkReq.getToken();
if (token == null || token == '') return false;
const res = await AkReq.request({
url: this.baseUrl + '/auth/v1/user',
method: 'GET',
headers: {
apikey: this.apikey,
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
} as UTSJSONObject
}, false);
const status = res.status ?? 0;
if (!(status >= 200 && status < 400)) {
return false;
}
let user: UTSJSONObject | null = null;
try {
user = new UTSJSONObject(res.data);
} catch (e) {
user = null;
}
if (user == null) return false;
this.user = user;
// 仅补齐最小 session 结构,供 getSession / UI 判断登录态使用
if (this.session == null) {
this.session = {
access_token: token,
refresh_token: AkReq.getRefreshToken() ?? '',
expires_at: AkReq.getExpiresAt() ?? 0,
user: user,
token_type: 'bearer',
expires_in: 0,
raw: user
} as AkSupaSignInResult;
}
return true;
} catch (e) {
return false;
}
}
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> {
// 提前检查 apikey 配置是否为占位符,避免发送无效请求导致 401
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: headers,
data: reqData,
contentType: 'application/json'
}, false);
// 如果响应不是 2xx例如 401提取后端错误信息并抛出便于上层显示具体原因
const status = res.status ?? 0;
if (!(status >= 200 && status < 400)) {
let msg = 'user.login.login_failed';
try {
if (res.data != null) {
const obj = new UTSJSONObject(res.data);
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
}
throw new Error(msg);
}
// 解析成功的返回体
let data: UTSJSONObject;
try {
data = new UTSJSONObject(res.data);
} catch (e) {
data = new 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');
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;
return session;
}
/**
* 获取当前 session 和 user
*/
getSession() : AkSupaSessionInfo {
return {
session: this.session,
user: this.user
};
}
async signUp(email : string, password : string, options : UTSJSONObject | null = null) : Promise<UTSJSONObject> {
const headers = new UTSJSONObject()
headers.set('apikey', this.apikey)
headers.set('Content-Type', 'application/json')
const data = new UTSJSONObject()
data.set('email', email)
data.set('password', password)
if (options != null) {
const dataField = options.getJSON('data')
if (dataField != null) {
data.set('data', dataField)
}
}
const res = await AkReq.request({
url: this.baseUrl + '/auth/v1/signup',
method: 'POST',
headers: headers,
data: data,
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 = new UTSJSONObject()
headers.set('apikey', this.apikey)
headers.set('Content-Type', 'application/json')
headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`)
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';
}
}
// 确保有 select 参数
if (options.columns == null) {
params.push('select=*');
} else if (options.columns == "") {
params.push('select=*');
}
} 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' | 'HEAD' = '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 = new UTSJSONObject()
headers.set('apikey', this.apikey)
headers.set('Content-Type', 'application/json')
headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`)
headers.set('Prefer', 'return=representation')
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 = new UTSJSONObject()
headers.set('apikey', this.apikey)
headers.set('Content-Type', 'application/json')
headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`)
headers.set('Prefer', 'return=representation')
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 = new UTSJSONObject()
headers.set('apikey', this.apikey)
headers.set('Content-Type', 'application/json')
headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`)
headers.set('Prefer', 'return=representation')
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 = new UTSJSONObject()
headers.set('apikey', this.apikey)
headers.set('Content-Type', 'application/json')
headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`)
let reqOptions : AkReqOptions = {
url,
method: 'POST',
headers,
data: params ?? new UTSJSONObject(),
contentType: 'application/json'
};
return await this.requestWithAutoRefresh(reqOptions);
}
/**
* 兼容 supabase-js 风格
* @param tableName 表名
*/
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: headers,
data: data,
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;
}
}
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);
// 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' });
console.log('登录已过期,请重新登录');
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('&');
}
/**
* 创建 Supabase 客户端实例
* @param url 项目 URL
* @param key 项目匿名密钥 (Anon Key)
*/
export function createClient(url : string, key : string) : AkSupa {
return new AkSupa(url, key);
}
// 模拟 Realtime Channel 类 (Polling Fallback)
export class AkSupaRealtimeChannel {
private _supa: AkSupa;
private _topic: string;
private _timer: number = 0;
private _callback: ((payload: any) => void) | null = null;
private _table: string = '';
private _lastTime: string = new Date().toISOString();
private _isSubscribed: boolean = false;
constructor(supa: AkSupa, topic: string) {
this._supa = supa;
this._topic = topic;
}
// 绑定事件 (仅支持 postgres_changes INSERT)
on(type: string, filter: UTSJSONObject, callback: (payload: any) => void): AkSupaRealtimeChannel {
// 解析 table
const table = filter.getString('table');
if (table != null) {
this._table = table;
}
this._callback = callback;
return this;
}
// 开始订阅
subscribe(callback?: (status: string, err: any | null) => void): AkSupaRealtimeChannel {
if (this._isSubscribed) return this;
this._isSubscribed = true;
// 初始回调
if (callback != null) {
callback('SUBSCRIBED', null);
}
// 如果没有指定 table无法轮询
if (this._table == '') {
console.warn('Realtime check: No table specified for polling.');
return this;
}
// 开始轮询 (每3秒)
this._timer = setInterval(() => {
this._checkUpdates();
}, 3000);
return this;
}
// 停止订阅
unsubscribe() {
if (this._timer > 0) {
clearInterval(this._timer);
this._timer = 0;
}
this._isSubscribed = false;
}
// 检查更新
private async _checkUpdates() {
if (!this._isSubscribed || this._table == '') return;
try {
const now = new Date().toISOString();
const res = await this._supa
.from(this._table)
.select('*')
.gt('created_at', this._lastTime)
.order('created_at', { ascending: true })
.execute();
if (res.error == null && res.data != null) {
let list: any[] = [];
if (Array.isArray(res.data)) {
list = res.data as any[];
}
if (list.length > 0) {
// 更新最后时间
const lastItem = list[list.length - 1];
let lastTimeStr: string | null = null;
if (lastItem instanceof UTSJSONObject) {
lastTimeStr = lastItem.getString('created_at');
} else {
// 尝试转 json
const j = JSON.parse(JSON.stringify(lastItem)) as UTSJSONObject;
lastTimeStr = j.getString('created_at');
}
if (lastTimeStr != null) {
this._lastTime = lastTimeStr;
} else {
this._lastTime = now;
}
// 触发回调
if (this._callback != null) {
// 模拟 Realtime payload
list.forEach(item => {
const payload = {
new: item,
eventType: 'INSERT',
old: null
};
this._callback?.(payload);
});
}
}
}
} catch (e) {
console.error('Realtime polling error:', e);
}
}
}
export default AkSupa;