2026-05-14 22:36:04 +08:00
|
|
|
package tableDB
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
2026-05-15 12:12:04 +08:00
|
|
|
"time"
|
2026-05-14 22:36:04 +08:00
|
|
|
|
|
|
|
|
"apigo.cc/go/cast"
|
|
|
|
|
"apigo.cc/go/db"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Table provides an AI-friendly interface for interacting with structured data or schema.
|
|
|
|
|
type Table struct {
|
|
|
|
|
Name string
|
|
|
|
|
userID string
|
|
|
|
|
db *db.DB
|
|
|
|
|
app *TableDB
|
|
|
|
|
constraint map[string]any
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
// NewTable creates a new Table instance. Handles ":Field" suffix.
|
2026-05-14 22:36:04 +08:00
|
|
|
func NewTable(name string, app *TableDB) *Table {
|
|
|
|
|
actualName := name
|
|
|
|
|
var constraint map[string]any
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
if strings.HasSuffix(name, ":Field") {
|
2026-05-14 22:36:04 +08:00
|
|
|
tableName := strings.TrimSuffix(name, ":Field")
|
|
|
|
|
actualName = "_Field"
|
|
|
|
|
// lookup table_id
|
|
|
|
|
res := app.base.Query("SELECT id FROM `_Table` WHERE name = ? LIMIT 1", tableName)
|
|
|
|
|
if res.Error == nil {
|
|
|
|
|
rec := res.MapOnR1()
|
|
|
|
|
if len(rec) > 0 {
|
|
|
|
|
constraint = map[string]any{"tableId": cast.String(rec["id"])}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &Table{
|
|
|
|
|
Name: actualName,
|
|
|
|
|
userID: app.userID,
|
|
|
|
|
db: app.base,
|
|
|
|
|
app: app,
|
|
|
|
|
constraint: constraint,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
func (t *Table) checkAuth(id string, action string) error {
|
|
|
|
|
if t.userID == SystemUserID {
|
2026-05-14 22:36:04 +08:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-05-15 12:12:04 +08:00
|
|
|
|
|
|
|
|
if action == "write" && (t.Name == "_Policy" || t.Name == "_Backup") {
|
|
|
|
|
return fmt.Errorf("permission denied for %s", t.Name)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
tableRec := GlobalCache.GetTable(t.Name)
|
2026-05-15 12:12:04 +08:00
|
|
|
enableRLS := false
|
|
|
|
|
if tableRec != nil {
|
|
|
|
|
enableRLS = cast.Bool(tableRec["enableRLS"])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !enableRLS && !strings.HasPrefix(t.Name, "_") {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
policies := GlobalCache.GetFlatPolicies(t.userID, t.Name, action)
|
|
|
|
|
hasFullAccess := false
|
|
|
|
|
for _, p := range policies {
|
|
|
|
|
if p.Condition == "" {
|
|
|
|
|
hasFullAccess = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if hasFullAccess {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建合并查询:一次性判断 Creator 和 所有 Conditions
|
|
|
|
|
var authConditions []string
|
|
|
|
|
var authArgs []any
|
|
|
|
|
|
|
|
|
|
// 1. 追加 Creator 检查
|
2026-05-16 01:04:46 +08:00
|
|
|
hasCreator := true
|
2026-05-14 23:39:37 +08:00
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
if hasCreator {
|
2026-05-16 01:04:46 +08:00
|
|
|
authConditions = append(authConditions, "`creator` = ?")
|
2026-05-15 12:12:04 +08:00
|
|
|
authArgs = append(authArgs, t.userID)
|
2026-05-14 23:39:37 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
// 2. 追加 Policy Conditions
|
|
|
|
|
for _, p := range policies {
|
|
|
|
|
if p.Condition != "" {
|
|
|
|
|
authConditions = append(authConditions, "("+p.Condition+")")
|
|
|
|
|
authArgs = append(authArgs, p.ConditionArgs...)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-15 12:12:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 组装并执行终极 1-RTT 查询
|
|
|
|
|
if len(authConditions) > 0 {
|
|
|
|
|
authPart := "(" + strings.Join(authConditions, " OR ") + ")"
|
|
|
|
|
checkSQL := fmt.Sprintf("SELECT 1 FROM `%s` WHERE id = ? AND %s LIMIT 1", t.Name, authPart)
|
|
|
|
|
|
|
|
|
|
finalArgs := append([]any{id}, authArgs...)
|
|
|
|
|
checkRes := t.db.Query(checkSQL, finalArgs...)
|
|
|
|
|
if len(checkRes.MapOnR1()) > 0 {
|
|
|
|
|
return nil // 验证通过!
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-15 12:12:04 +08:00
|
|
|
|
|
|
|
|
return fmt.Errorf("permission denied for %s record %s", t.Name, id)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *Table) appendAuthAndConstraint(whereStr string, args []any) (string, []any, error) {
|
|
|
|
|
if t.constraint != nil {
|
|
|
|
|
for k, v := range t.constraint {
|
|
|
|
|
if whereStr != "" {
|
|
|
|
|
whereStr += fmt.Sprintf(" AND %s = ?", k)
|
|
|
|
|
} else {
|
|
|
|
|
whereStr = fmt.Sprintf("%s = ?", k)
|
|
|
|
|
}
|
|
|
|
|
args = append(args, v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
if t.userID == SystemUserID {
|
2026-05-14 23:39:37 +08:00
|
|
|
return whereStr, args, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tableRec := GlobalCache.GetTable(t.Name)
|
2026-05-15 12:12:04 +08:00
|
|
|
enableRLS := false
|
|
|
|
|
if tableRec != nil {
|
|
|
|
|
enableRLS = cast.Bool(tableRec["enableRLS"])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !enableRLS && !strings.HasPrefix(t.Name, "_") {
|
|
|
|
|
return whereStr, args, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
policies := GlobalCache.GetFlatPolicies(t.userID, t.Name, "read")
|
|
|
|
|
hasFullAccess := false
|
|
|
|
|
for _, p := range policies {
|
|
|
|
|
if p.Condition == "" {
|
|
|
|
|
hasFullAccess = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if hasFullAccess {
|
|
|
|
|
return whereStr, args, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build dynamic SQL
|
|
|
|
|
var authConditions []string
|
|
|
|
|
var authArgs []any
|
|
|
|
|
|
|
|
|
|
// Check creator field
|
2026-05-16 01:04:46 +08:00
|
|
|
hasCreator := true
|
2026-05-14 23:39:37 +08:00
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
if hasCreator {
|
2026-05-16 01:04:46 +08:00
|
|
|
authConditions = append(authConditions, "`creator` = ?")
|
2026-05-15 12:12:04 +08:00
|
|
|
authArgs = append(authArgs, t.userID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, p := range policies {
|
|
|
|
|
if p.Condition != "" {
|
|
|
|
|
authConditions = append(authConditions, "("+p.Condition+")")
|
|
|
|
|
authArgs = append(authArgs, p.ConditionArgs...)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-15 12:12:04 +08:00
|
|
|
|
|
|
|
|
authPart := "0"
|
|
|
|
|
if len(authConditions) > 0 {
|
|
|
|
|
authPart = "(" + strings.Join(authConditions, " OR ") + ")"
|
|
|
|
|
args = append(args, authArgs...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if whereStr != "" {
|
|
|
|
|
whereStr = "(" + whereStr + ") AND " + authPart
|
|
|
|
|
} else {
|
|
|
|
|
whereStr = authPart
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 22:36:04 +08:00
|
|
|
return whereStr, args, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *Table) reconstructAndSyncSchema() error {
|
|
|
|
|
tables := t.db.Query("SELECT * FROM `_Table`").MapResults()
|
|
|
|
|
fields := t.db.Query("SELECT * FROM `_Field` ORDER BY tableId").MapResults()
|
|
|
|
|
|
|
|
|
|
fieldMap := make(map[string][]map[string]any)
|
|
|
|
|
for _, f := range fields {
|
|
|
|
|
tid := cast.String(f["tableId"])
|
|
|
|
|
fieldMap[tid] = append(fieldMap[tid], f)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var sb strings.Builder
|
|
|
|
|
for _, tbl := range tables {
|
|
|
|
|
name := cast.String(tbl["name"])
|
2026-05-15 12:12:04 +08:00
|
|
|
if name == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
tid := cast.String(tbl["id"])
|
|
|
|
|
tblFields := fieldMap[tid]
|
|
|
|
|
if len(tblFields) == 0 {
|
|
|
|
|
continue // Skip tables with no fields to avoid SQL errors
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 22:36:04 +08:00
|
|
|
memo := cast.String(tbl["memo"])
|
|
|
|
|
sb.WriteString(name + " SD")
|
|
|
|
|
if memo != "" {
|
|
|
|
|
sb.WriteString(" //" + strings.ReplaceAll(memo, "\n", " "))
|
|
|
|
|
}
|
|
|
|
|
sb.WriteString("\n")
|
|
|
|
|
|
2026-05-16 01:04:46 +08:00
|
|
|
// Always ensure standard fields are present with appropriate indices
|
|
|
|
|
sb.WriteString(" id c10 PK\n")
|
|
|
|
|
sb.WriteString(" createTime bi I\n")
|
|
|
|
|
sb.WriteString(" creator v64\n")
|
|
|
|
|
sb.WriteString(" updateTime bi I\n")
|
|
|
|
|
sb.WriteString(" updater v64\n")
|
2026-05-14 23:39:37 +08:00
|
|
|
|
|
|
|
|
for _, f := range tblFields {
|
2026-05-14 22:36:04 +08:00
|
|
|
fname := cast.String(f["name"])
|
2026-05-16 01:04:46 +08:00
|
|
|
if fname == "id" || fname == "createTime" || fname == "creator" || fname == "updateTime" || fname == "updater" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
ftype := cast.String(f["type"])
|
2026-05-15 12:12:04 +08:00
|
|
|
if ftype == "" {
|
|
|
|
|
ftype = "v255"
|
|
|
|
|
}
|
|
|
|
|
isIndex := cast.Bool(f["isIndex"]) || cast.Int(f["isIndex"]) == 1
|
2026-05-14 22:36:04 +08:00
|
|
|
fmemo := cast.String(f["memo"])
|
2026-05-15 12:12:04 +08:00
|
|
|
|
2026-05-14 22:36:04 +08:00
|
|
|
sb.WriteString(" " + fname + " " + ftype)
|
|
|
|
|
if isIndex {
|
|
|
|
|
sb.WriteString(" I")
|
|
|
|
|
}
|
|
|
|
|
if fmemo != "" {
|
|
|
|
|
sb.WriteString(" //" + strings.ReplaceAll(fmemo, "\n", " "))
|
|
|
|
|
}
|
|
|
|
|
sb.WriteString("\n")
|
|
|
|
|
}
|
|
|
|
|
sb.WriteString("\n")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 01:04:46 +08:00
|
|
|
err := t.db.Sync(sb.String())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return GlobalCache.Load(t.app)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
// Set performs an upsert of one or more records.
|
|
|
|
|
func (t *Table) Set(data ...any) error {
|
2026-05-14 22:36:04 +08:00
|
|
|
if t.userID == "" {
|
2026-05-15 12:12:04 +08:00
|
|
|
return fmt.Errorf("authentication required")
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
metaTouched := false
|
2026-05-16 01:04:46 +08:00
|
|
|
var updatedRows []map[string]any
|
|
|
|
|
|
|
|
|
|
// Pre-fetch schema context for business tables
|
|
|
|
|
var tableSchema *TableSchema
|
|
|
|
|
var fields []FieldSchema
|
|
|
|
|
tableRec := GlobalCache.GetTable(t.Name)
|
|
|
|
|
if tableRec != nil {
|
|
|
|
|
cast.Convert(&tableSchema, tableRec)
|
|
|
|
|
fields = GlobalCache.GetFields(tableSchema.ID)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
for _, d := range data {
|
|
|
|
|
record := make(map[string]any)
|
|
|
|
|
cast.Convert(&record, d)
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
if t.constraint != nil {
|
|
|
|
|
for k, v := range t.constraint {
|
|
|
|
|
record[k] = v
|
|
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
if t.app.hooks.OnUpdatingRow != nil && t.Name != "_Table" && t.Name != "_Field" {
|
2026-05-16 01:04:46 +08:00
|
|
|
if err := t.app.hooks.OnUpdatingRow(record, tableSchema, fields); err != nil {
|
2026-05-15 12:12:04 +08:00
|
|
|
return err
|
|
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
idVal := record["id"]
|
|
|
|
|
var isInsert bool
|
|
|
|
|
if idVal == nil || cast.String(idVal) == "" {
|
2026-05-14 22:36:04 +08:00
|
|
|
isInsert = true
|
2026-05-15 12:12:04 +08:00
|
|
|
} else {
|
|
|
|
|
idStr := cast.String(idVal)
|
|
|
|
|
res := t.db.Query(fmt.Sprintf("SELECT id FROM `%s` WHERE id = ? LIMIT 1", t.Name), idStr)
|
|
|
|
|
rec := res.MapOnR1()
|
|
|
|
|
if rec != nil && len(rec) > 0 {
|
|
|
|
|
if err := t.checkAuth(idStr, "write"); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
isInsert = false
|
|
|
|
|
} else {
|
|
|
|
|
isInsert = true
|
|
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 01:04:46 +08:00
|
|
|
// Always update updateTime and updater
|
|
|
|
|
record["updateTime"] = time.Now().UnixMilli()
|
|
|
|
|
record["updater"] = t.userID
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
var err error
|
|
|
|
|
if isInsert {
|
2026-05-16 01:04:46 +08:00
|
|
|
record["createTime"] = record["updateTime"]
|
2026-05-15 12:12:04 +08:00
|
|
|
if t.userID != SystemUserID {
|
2026-05-16 01:04:46 +08:00
|
|
|
if t.Name == "_Policy" || t.Name == "_Backup" {
|
|
|
|
|
return fmt.Errorf("permission denied for %s", t.Name)
|
2026-05-15 12:12:04 +08:00
|
|
|
}
|
2026-05-16 01:04:46 +08:00
|
|
|
record["creator"] = t.userID
|
2026-05-15 12:12:04 +08:00
|
|
|
} else {
|
2026-05-16 01:04:46 +08:00
|
|
|
if record["creator"] == nil {
|
|
|
|
|
record["creator"] = t.userID
|
2026-05-14 23:39:37 +08:00
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-15 12:12:04 +08:00
|
|
|
err = t.db.Insert(t.Name, record).Error
|
|
|
|
|
} else {
|
2026-05-16 01:04:46 +08:00
|
|
|
// Prevent overwriting CreateTime and Creator on update
|
|
|
|
|
delete(record, "createTime")
|
|
|
|
|
delete(record, "creator")
|
2026-05-15 12:12:04 +08:00
|
|
|
err = t.db.Update(t.Name, record, "id = ?", cast.String(record["id"])).Error
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if t.Name == "_Table" || t.Name == "_Field" || t.Name == "_Policy" {
|
|
|
|
|
metaTouched = true
|
|
|
|
|
if t.Name == "_Table" {
|
|
|
|
|
if isInsert && t.app.hooks.OnCreatedTable != nil {
|
2026-05-16 01:04:46 +08:00
|
|
|
var ts TableSchema
|
|
|
|
|
cast.Convert(&ts, record)
|
|
|
|
|
t.app.hooks.OnCreatedTable(&ts)
|
2026-05-15 12:12:04 +08:00
|
|
|
}
|
|
|
|
|
} else if t.Name == "_Field" {
|
|
|
|
|
if t.app.hooks.OnUpdatedField != nil {
|
2026-05-16 01:04:46 +08:00
|
|
|
var fs FieldSchema
|
|
|
|
|
cast.Convert(&fs, record)
|
|
|
|
|
// Look up table schema
|
|
|
|
|
var fts *TableSchema
|
|
|
|
|
GlobalCache.lock.RLock()
|
|
|
|
|
tableName := GlobalCache.TableIDMap[fs.TableID]
|
|
|
|
|
fTableRec := GlobalCache.Tables[tableName]
|
|
|
|
|
GlobalCache.lock.RUnlock()
|
|
|
|
|
if fTableRec != nil {
|
|
|
|
|
cast.Convert(&fts, fTableRec)
|
|
|
|
|
}
|
|
|
|
|
t.app.hooks.OnUpdatedField(fts, &fs)
|
2026-05-15 12:12:04 +08:00
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-15 12:12:04 +08:00
|
|
|
} else {
|
2026-05-16 01:04:46 +08:00
|
|
|
updatedRows = append(updatedRows, record)
|
2026-05-15 12:12:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if metaTouched {
|
|
|
|
|
if t.Name != "_Policy" {
|
|
|
|
|
_ = t.reconstructAndSyncSchema()
|
|
|
|
|
}
|
|
|
|
|
_ = GlobalCache.Load(t.app)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 01:04:46 +08:00
|
|
|
if len(updatedRows) > 0 && t.app.hooks.OnUpdatedRows != nil {
|
|
|
|
|
t.app.hooks.OnUpdatedRows(updatedRows, tableSchema, fields)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetField adds or updates one or more fields. If table doesn't exist, it will be created.
|
|
|
|
|
func (t *Table) SetField(fields ...FieldSchema) error {
|
|
|
|
|
if t.userID == "" {
|
|
|
|
|
return fmt.Errorf("authentication required")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1. Ensure table exists in _Table
|
|
|
|
|
tableRec := GlobalCache.GetTable(t.Name)
|
|
|
|
|
if tableRec == nil {
|
|
|
|
|
// Create table entry
|
|
|
|
|
err := t.app.SetTable(TableSchema{Name: t.Name})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tableRec = GlobalCache.GetTable(t.Name)
|
|
|
|
|
if tableRec == nil {
|
|
|
|
|
return fmt.Errorf("failed to create table entry for %s", t.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Automatically add default ID field to metadata
|
|
|
|
|
_ = t.app.Table(t.Name + ":Field").Set(map[string]any{
|
|
|
|
|
"name": "id",
|
|
|
|
|
"type": "c10",
|
|
|
|
|
"isIndex": 1,
|
|
|
|
|
"memo": "Primary Key",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Prepare field records
|
|
|
|
|
fieldTable := t.app.Table(t.Name + ":Field")
|
|
|
|
|
var fieldRecords []any
|
|
|
|
|
for _, f := range fields {
|
|
|
|
|
fRecord := map[string]any{
|
|
|
|
|
"name": f.Name,
|
|
|
|
|
"type": f.Type,
|
|
|
|
|
"isIndex": f.IsIndex,
|
|
|
|
|
"memo": f.Memo,
|
|
|
|
|
"settings": f.Settings,
|
|
|
|
|
}
|
|
|
|
|
if f.ID != "" {
|
|
|
|
|
fRecord["id"] = f.ID
|
2026-05-14 22:36:04 +08:00
|
|
|
} else {
|
2026-05-15 12:12:04 +08:00
|
|
|
// If ID is missing, try to find existing field to update
|
|
|
|
|
existing, _ := fieldTable.List(map[string]any{"name": f.Name})
|
|
|
|
|
if len(existing) > 0 {
|
|
|
|
|
fRecord["id"] = existing[0]["id"]
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-15 12:12:04 +08:00
|
|
|
fieldRecords = append(fieldRecords, fRecord)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
// 3. Batch Set fields
|
|
|
|
|
return fieldTable.Set(fieldRecords...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RemoveField deletes one or more fields by name.
|
|
|
|
|
func (t *Table) RemoveField(names ...string) error {
|
|
|
|
|
if t.userID == "" {
|
|
|
|
|
return fmt.Errorf("authentication required")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tableRec := GlobalCache.GetTable(t.Name)
|
|
|
|
|
if tableRec == nil {
|
|
|
|
|
return fmt.Errorf("table %s not found", t.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fieldTable := t.app.Table(t.Name + ":Field")
|
|
|
|
|
for _, name := range names {
|
|
|
|
|
existing, _ := fieldTable.List(map[string]any{"name": name})
|
|
|
|
|
if len(existing) > 0 {
|
|
|
|
|
err := fieldTable.Remove(cast.String(existing[0]["id"]))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get retrieves a single record.
|
|
|
|
|
func (t *Table) Get(id string) (map[string]any, error) {
|
2026-05-15 12:12:04 +08:00
|
|
|
if t.userID == "" {
|
|
|
|
|
return nil, fmt.Errorf("authentication required")
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
if err := t.checkAuth(id, "read"); err != nil {
|
2026-05-14 22:36:04 +08:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? LIMIT 1", t.Name)
|
|
|
|
|
res := t.db.Query(query, id)
|
|
|
|
|
if res.Error != nil {
|
|
|
|
|
return nil, res.Error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
record := res.MapOnR1()
|
|
|
|
|
if len(record) == 0 {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
return record, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
// Remove deletes one or more records by ID.
|
|
|
|
|
func (t *Table) Remove(ids ...string) error {
|
|
|
|
|
if t.userID == "" {
|
|
|
|
|
return fmt.Errorf("authentication required")
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 01:04:46 +08:00
|
|
|
var removedIDs []string
|
|
|
|
|
|
|
|
|
|
// Pre-fetch schema context for business tables
|
|
|
|
|
var tableSchema *TableSchema
|
|
|
|
|
var fields []FieldSchema
|
|
|
|
|
tableRec := GlobalCache.GetTable(t.Name)
|
|
|
|
|
if tableRec != nil {
|
|
|
|
|
cast.Convert(&tableSchema, tableRec)
|
|
|
|
|
fields = GlobalCache.GetFields(tableSchema.ID)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
for _, id := range ids {
|
|
|
|
|
if err := t.checkAuth(id, "write"); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
var record map[string]any
|
|
|
|
|
if t.Name == "_Table" || t.Name == "_Field" || t.Name == "_Policy" {
|
|
|
|
|
res := t.db.Query(fmt.Sprintf("SELECT * FROM `%s` WHERE id = ?", t.Name), id)
|
|
|
|
|
record = res.MapOnR1()
|
|
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
res := t.db.Delete(t.Name, "id = ?", id)
|
|
|
|
|
if res.Error == nil {
|
|
|
|
|
if t.Name == "_Table" || t.Name == "_Field" || t.Name == "_Policy" {
|
|
|
|
|
if t.Name != "_Policy" {
|
|
|
|
|
_ = t.reconstructAndSyncSchema()
|
|
|
|
|
}
|
|
|
|
|
_ = GlobalCache.Load(t.app)
|
|
|
|
|
|
|
|
|
|
if t.Name == "_Table" && record != nil && t.app.hooks.OnRemovedTable != nil {
|
2026-05-16 01:04:46 +08:00
|
|
|
var ts TableSchema
|
|
|
|
|
cast.Convert(&ts, record)
|
|
|
|
|
t.app.hooks.OnRemovedTable(&ts)
|
2026-05-15 12:12:04 +08:00
|
|
|
} else if t.Name == "_Field" && record != nil && t.app.hooks.OnRemovedField != nil {
|
2026-05-16 01:04:46 +08:00
|
|
|
var fts *TableSchema
|
|
|
|
|
GlobalCache.lock.RLock()
|
|
|
|
|
tableName := GlobalCache.TableIDMap[cast.String(record["tableId"])]
|
|
|
|
|
fTableRec := GlobalCache.Tables[tableName]
|
|
|
|
|
GlobalCache.lock.RUnlock()
|
|
|
|
|
if fTableRec != nil {
|
|
|
|
|
cast.Convert(&fts, fTableRec)
|
|
|
|
|
}
|
|
|
|
|
t.app.hooks.OnRemovedField(fts, cast.String(record["name"]))
|
2026-05-15 12:12:04 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-05-16 01:04:46 +08:00
|
|
|
removedIDs = append(removedIDs, id)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-05-15 12:12:04 +08:00
|
|
|
return res.Error
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-16 01:04:46 +08:00
|
|
|
|
|
|
|
|
if len(removedIDs) > 0 && t.app.hooks.OnRemovedRows != nil {
|
|
|
|
|
t.app.hooks.OnRemovedRows(removedIDs, tableSchema, fields)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
return nil
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// List retrieves multiple records.
|
|
|
|
|
func (t *Table) List(where any, args ...any) ([]map[string]any, error) {
|
2026-05-16 01:04:46 +08:00
|
|
|
req := QueryRequest{}
|
2026-05-14 22:36:04 +08:00
|
|
|
if where != nil {
|
|
|
|
|
switch v := where.(type) {
|
|
|
|
|
case string:
|
2026-05-16 01:04:46 +08:00
|
|
|
req.Where = v
|
|
|
|
|
req.Args = args
|
2026-05-14 22:36:04 +08:00
|
|
|
case map[string]any:
|
2026-05-16 01:04:46 +08:00
|
|
|
req.Where, req.Args = buildWhere(v)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 01:04:46 +08:00
|
|
|
return t.Query(req)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:12:04 +08:00
|
|
|
// Query performs a structured query on the current table.
|
|
|
|
|
func (t *Table) Query(req QueryRequest) ([]map[string]any, error) {
|
|
|
|
|
if t.userID == "" {
|
|
|
|
|
return nil, fmt.Errorf("authentication required")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 01:04:46 +08:00
|
|
|
// Apply constraint to where clause
|
|
|
|
|
if t.constraint != nil {
|
|
|
|
|
if req.Where != "" {
|
|
|
|
|
// If it's a string, this is complex to merge safely without parsing SQL.
|
|
|
|
|
// Let's rely on appendAuthAndConstraint passing the constraint.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sql, args, err := t.app.buildQuery(t, req)
|
2026-05-15 12:12:04 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
res := t.db.Query(sql, args...)
|
|
|
|
|
if res.Error != nil {
|
|
|
|
|
return nil, res.Error
|
|
|
|
|
}
|
|
|
|
|
return res.MapResults(), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 22:36:04 +08:00
|
|
|
// Count returns the number of records.
|
|
|
|
|
func (t *Table) Count(where any, args ...any) (int64, error) {
|
2026-05-15 12:12:04 +08:00
|
|
|
if t.userID == "" {
|
|
|
|
|
return 0, fmt.Errorf("authentication required")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 22:36:04 +08:00
|
|
|
query := fmt.Sprintf("SELECT COUNT(*) FROM `%s` ", t.Name)
|
|
|
|
|
whereStr := ""
|
|
|
|
|
if where != nil {
|
|
|
|
|
switch v := where.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
whereStr = v
|
|
|
|
|
case map[string]any:
|
|
|
|
|
w, a := buildWhere(v)
|
|
|
|
|
whereStr = w
|
|
|
|
|
args = a
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
whereStr, args, err = t.appendAuthAndConstraint(whereStr, args)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if whereStr != "" {
|
|
|
|
|
query += " WHERE " + whereStr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res := t.db.Query(query, args...)
|
|
|
|
|
if res.Error != nil {
|
|
|
|
|
return 0, res.Error
|
|
|
|
|
}
|
|
|
|
|
return res.IntOnR1C1(), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CountBy returns counts grouped by a field.
|
|
|
|
|
func (t *Table) CountBy(field string) (map[any]int64, error) {
|
2026-05-15 12:12:04 +08:00
|
|
|
if t.userID == "" {
|
|
|
|
|
return nil, fmt.Errorf("authentication required")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 22:36:04 +08:00
|
|
|
query := fmt.Sprintf("SELECT `%s`, COUNT(*) as cnt FROM `%s` ", field, t.Name)
|
2026-05-15 12:12:04 +08:00
|
|
|
|
2026-05-14 22:36:04 +08:00
|
|
|
whereStr, args, err := t.appendAuthAndConstraint("", nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if whereStr != "" {
|
|
|
|
|
query += " WHERE " + whereStr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += fmt.Sprintf(" GROUP BY `%s` ", field)
|
2026-05-15 12:12:04 +08:00
|
|
|
|
2026-05-14 22:36:04 +08:00
|
|
|
res := t.db.Query(query, args...)
|
|
|
|
|
if res.Error != nil {
|
|
|
|
|
return nil, res.Error
|
|
|
|
|
}
|
|
|
|
|
rows := res.MapResults()
|
|
|
|
|
result := make(map[any]int64)
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
result[row[field]] = cast.Int64(row["cnt"])
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fields returns field metadata.
|
|
|
|
|
func (t *Table) Fields() ([]FieldSchema, error) {
|
2026-05-15 12:12:04 +08:00
|
|
|
if t.userID == "" {
|
|
|
|
|
return nil, fmt.Errorf("authentication required")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 22:36:04 +08:00
|
|
|
tid := ""
|
|
|
|
|
tableRecord := GlobalCache.GetTable(t.Name)
|
|
|
|
|
if tableRecord != nil {
|
|
|
|
|
tid = cast.String(tableRecord["id"])
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("table metadata not found in cache: %s", t.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return GlobalCache.GetFields(tid), nil
|
|
|
|
|
}
|