tableDB/table.go

468 lines
11 KiB
Go

package tableDB
import (
"fmt"
"strings"
"apigo.cc/go/cast"
"apigo.cc/go/db"
"apigo.cc/go/id"
)
// 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
isMeta bool // True if Name ends with ":Schema"
constraint map[string]any
}
// NewTable creates a new Table instance. Handles ":Schema" and ":Field" suffix.
func NewTable(name string, app *TableDB) *Table {
isMeta := false
actualName := name
var constraint map[string]any
if strings.HasSuffix(name, ":Schema") {
isMeta = true
actualName = strings.TrimSuffix(name, ":Schema")
} else if strings.HasSuffix(name, ":Field") {
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,
isMeta: isMeta,
constraint: constraint,
}
}
func (t *Table) checkAuth(id string) error {
if t.userID == "_system" {
return nil
}
// Check if table has 'creator' field
hasCreator := false
tableRec := GlobalCache.GetTable(t.Name)
if tableRec != nil {
tid := cast.String(tableRec["id"])
fields := GlobalCache.GetFields(tid)
for _, f := range fields {
if f.Name == "creator" {
hasCreator = true
break
}
}
}
if !hasCreator && !strings.HasPrefix(t.Name, "_") {
return nil
}
if t.Name == "_Policy" || t.Name == "_Backup" {
return fmt.Errorf("permission denied for %s", t.Name)
}
// Check existing record
res := t.db.Query(fmt.Sprintf("SELECT creator FROM `%s` WHERE id = ?", t.Name), id)
rec := res.MapOnR1()
if len(rec) > 0 {
if cast.String(rec["creator"]) == t.userID {
return nil
}
// Check _Policy
pol := t.db.Query("SELECT 1 FROM _Policy WHERE subject = ? AND resource = ? AND effect = 'allow' LIMIT 1", t.userID, id)
if len(pol.MapOnR1()) > 0 {
return nil
}
return fmt.Errorf("permission denied for %s record %s", t.Name, id)
}
return nil
}
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)
}
}
if t.userID == "_system" {
return whereStr, args, nil
}
// Check if table has 'creator' field
hasCreator := false
tableRec := GlobalCache.GetTable(t.Name)
if tableRec != nil {
tid := cast.String(tableRec["id"])
fields := GlobalCache.GetFields(tid)
for _, f := range fields {
if f.Name == "creator" {
hasCreator = true
break
}
}
}
if hasCreator || strings.HasPrefix(t.Name, "_") {
if t.Name == "_Policy" || t.Name == "_Backup" {
return "", nil, fmt.Errorf("permission denied for %s", t.Name)
}
authWhere := fmt.Sprintf("(creator = '%s' OR id IN (SELECT resource FROM _Policy WHERE subject = '%s' AND effect = 'allow'))", t.userID, t.userID)
if whereStr != "" {
whereStr = "(" + whereStr + ") AND " + authWhere
} else {
whereStr = authWhere
}
}
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"])
if name == "" { continue }
tid := cast.String(tbl["id"])
tblFields := fieldMap[tid]
if len(tblFields) == 0 {
continue // Skip tables with no fields to avoid SQL errors
}
memo := cast.String(tbl["memo"])
sb.WriteString(name + " SD")
if memo != "" {
sb.WriteString(" //" + strings.ReplaceAll(memo, "\n", " "))
}
sb.WriteString("\n")
hasID := false
for _, f := range tblFields {
if cast.String(f["name"]) == "id" {
hasID = true
break
}
}
if !hasID {
sb.WriteString(" id c10 PK\n")
}
for _, f := range tblFields {
fname := cast.String(f["name"])
ftype := cast.String(f["type"])
if ftype == "" { ftype = "v255" }
isIndex := cast.Int(f["isIndex"]) == 1
fmemo := cast.String(f["memo"])
sb.WriteString(" " + fname + " " + ftype)
if isIndex {
sb.WriteString(" I")
}
if fmemo != "" {
sb.WriteString(" //" + strings.ReplaceAll(fmemo, "\n", " "))
}
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return t.db.Sync(sb.String())
}
// Set performs an upsert.
func (t *Table) Set(data any) error {
if t.userID == "" {
return fmt.Errorf("no permission")
}
record := make(map[string]any)
j, _ := cast.ToJSON(data)
_ = cast.UnmarshalJSON([]byte(j), &record)
if t.isMeta {
dsl, ok := record["dsl"].(string)
if !ok {
return fmt.Errorf("schema dsl is required")
}
return t.app.SyncSchema(dsl)
}
if t.constraint != nil {
for k, v := range t.constraint {
record[k] = v
}
}
if t.app.Hooks.OnUpdatingRow != nil && t.Name != "_Table" && t.Name != "_Field" {
if err := t.app.Hooks.OnUpdatingRow(t.Name, record); err != nil {
return err
}
}
idVal := record["id"]
var isInsert bool
if idVal == nil || cast.String(idVal) == "" {
record["id"] = id.MakeID(10)
isInsert = true
} 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); err != nil {
return err
}
isInsert = false
} else {
isInsert = true
}
}
var err error
if isInsert {
if t.userID != "_system" {
// Check if table has 'creator' field
hasCreator := false
tableRec := GlobalCache.GetTable(t.Name)
if tableRec != nil {
tid := cast.String(tableRec["id"])
fields := GlobalCache.GetFields(tid)
for _, f := range fields {
if f.Name == "creator" {
hasCreator = true
break
}
}
}
if hasCreator || strings.HasPrefix(t.Name, "_") {
if t.Name == "_Policy" || t.Name == "_Backup" {
return fmt.Errorf("permission denied for %s", t.Name)
}
record["creator"] = t.userID
}
}
err = t.db.Insert(t.Name, record).Error
} else {
err = t.db.Update(t.Name, record, "id = ?", cast.String(record["id"])).Error
}
if err == nil {
if t.Name == "_Table" {
_ = t.reconstructAndSyncSchema()
if isInsert && t.app.Hooks.OnCreatedTable != nil {
t.app.Hooks.OnCreatedTable(cast.String(record["name"]), record)
}
} else if t.Name == "_Field" {
_ = t.reconstructAndSyncSchema()
if t.app.Hooks.OnUpdatedField != nil {
t.app.Hooks.OnUpdatedField(cast.String(record["tableId"]), cast.String(record["name"]), record)
}
} else {
if t.app.Hooks.OnUpdatedRows != nil {
t.app.Hooks.OnUpdatedRows(t.Name, 1)
}
}
}
return err
}
// Get retrieves a single record.
func (t *Table) Get(id string) (map[string]any, error) {
if t.isMeta {
return map[string]any{"name": t.Name, "type": "table"}, nil
}
if err := t.checkAuth(id); err != nil {
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
}
// Remove deletes a record.
func (t *Table) Remove(id string) error {
if t.isMeta {
return fmt.Errorf("schema removal not allowed via Table API")
}
if err := t.checkAuth(id); err != nil {
return err
}
var record map[string]any
if t.Name == "_Table" || t.Name == "_Field" {
res := t.db.Query(fmt.Sprintf("SELECT * FROM `%s` WHERE id = ?", t.Name), id)
record = res.MapOnR1()
}
res := t.db.Delete(t.Name, "id = ?", id)
if res.Error == nil {
if t.Name == "_Table" {
_ = t.reconstructAndSyncSchema()
if record != nil && t.app.Hooks.OnRemovedTable != nil {
t.app.Hooks.OnRemovedTable(cast.String(record["name"]))
}
} else if t.Name == "_Field" {
_ = t.reconstructAndSyncSchema()
if record != nil && t.app.Hooks.OnRemovedField != nil {
t.app.Hooks.OnRemovedField(cast.String(record["tableId"]), cast.String(record["name"]))
}
} else {
if t.app.Hooks.OnRemovedRows != nil {
t.app.Hooks.OnRemovedRows(t.Name, []string{id})
}
}
}
return res.Error
}
// List retrieves multiple records.
func (t *Table) List(where any, args ...any) ([]map[string]any, error) {
if t.isMeta {
return nil, fmt.Errorf("list not supported on schema tables")
}
query := fmt.Sprintf("SELECT * FROM `%s` ", t.Name)
whereStr := ""
if where != nil {
switch v := where.(type) {
case string:
whereStr = v
case map[string]any:
whereStr, args = buildWhere(v)
}
}
var err error
whereStr, args, err = t.appendAuthAndConstraint(whereStr, args)
if err != nil {
return nil, err
}
if whereStr != "" {
query += " WHERE " + whereStr
}
res := t.db.Query(query, args...)
if res.Error != nil {
return nil, res.Error
}
return res.MapResults(), nil
}
// Count returns the number of records.
func (t *Table) Count(where any, args ...any) (int64, error) {
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) {
query := fmt.Sprintf("SELECT `%s`, COUNT(*) as cnt FROM `%s` ", field, t.Name)
whereStr, args, err := t.appendAuthAndConstraint("", nil)
if err != nil {
return nil, err
}
if whereStr != "" {
query += " WHERE " + whereStr
}
query += fmt.Sprintf(" GROUP BY `%s` ", field)
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) {
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
}