feat: 新增 WithSilent/WithRolesScope 写操作选项
WithSilent 让写操作跳过变更通知(ResChangeEventFunc/ResWatcher),权限过滤/字段脱敏不受影响,用于类似"记录登录 IP"这种高频写但不需要广播"资源变了"的场景,避免不必要的缓存刷新。 WithRolesScope 显式指定权限过滤范围(ResScopeOn/All 才按角色过滤,Off 或不传都跳过),替代原来一直没接线生效的 Access 选项。两个选项相互独立,可以单独用也可以一起用。
This commit is contained in:
Executable
+52
@@ -0,0 +1,52 @@
|
||||
package req
|
||||
|
||||
import "git.fsdpf.net/go/db"
|
||||
|
||||
type ResOptions struct {
|
||||
Tx *db.TxDatabase
|
||||
Alias string
|
||||
Silent bool
|
||||
// RolesScope 默认零值 ResScopeOff:不传 WithRolesScope,或显式传 ResScopeOff,都表示跳过
|
||||
// 权限过滤;只有显式传 ResScopeOn/ResScopeAll 才会按角色过滤。
|
||||
RolesScope ResScope
|
||||
}
|
||||
|
||||
type ResOption func(p *ResOptions)
|
||||
|
||||
func WithTx(tx *db.TxDatabase) ResOption {
|
||||
return func(p *ResOptions) {
|
||||
p.Tx = tx
|
||||
}
|
||||
}
|
||||
|
||||
func WithAlias(as string) ResOption {
|
||||
return func(p *ResOptions) {
|
||||
p.Alias = as
|
||||
}
|
||||
}
|
||||
|
||||
// WithSilent 让这次写操作跳过变更通知(ResChangeEventFunc/ResWatcher),权限过滤、字段脱敏、
|
||||
// 字段规范化、类型转换、默认值填充等都不受影响。用于类似"记录登录 IP"这种高频写、但不需要广播
|
||||
// "资源变了"的场景,避免不必要的缓存刷新/watcher 触发。
|
||||
func WithSilent() ResOption {
|
||||
return func(p *ResOptions) {
|
||||
p.Silent = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithRolesScope 显式指定这次写/查操作的权限过滤范围。ResScopeOn/ResScopeAll 按角色过滤
|
||||
// (行级 WHERE 注入、字段级读脱敏、字段级写权限);ResScopeOff(或不调用这个 option)跳过
|
||||
// 权限过滤,变更通知、字段规范化、类型转换、默认值填充等不受影响。
|
||||
func WithRolesScope(s ResScope) ResOption {
|
||||
return func(p *ResOptions) {
|
||||
p.RolesScope = s
|
||||
}
|
||||
}
|
||||
|
||||
type gParamsOpt func(g *gparams)
|
||||
|
||||
func Session(session string) gParamsOpt {
|
||||
return func(g *gparams) {
|
||||
g.session = session
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,928 @@
|
||||
package resx
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/db/exec"
|
||||
"git.fsdpf.net/go/db/exp"
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
// changeRow 保存一行数据的主键、UPDATE/DELETE 前的快照,以及 INSERT/UPDATE 时写入的字段值
|
||||
type changeRow struct {
|
||||
id int64
|
||||
snapshot map[string]any
|
||||
changes map[string]any
|
||||
}
|
||||
|
||||
// dataProcessor 是挂在 db.SelectDataset/InsertDataset/UpdateDataset/DeleteDataset 上的读写钩子。
|
||||
//
|
||||
// 职责:
|
||||
// 1. 权限过滤:根据 DataInterceptor 返回的角色条件改写 SELECT/UPDATE/DELETE 的 WHERE。
|
||||
// 2. 数据规范化:INSERT/UPDATE 前统一清理系统保留字段、按字段类型转换保存值,并为 INSERT 填充默认值。
|
||||
// 3. 变更留痕:写操作完成后无条件调用 ResChangeEventFunc;当 DataInterceptor 判断需要时,写操作前
|
||||
// 抓取快照/写入值,写操作后额外调用 ResChangeRowFunc(由上层实现变更日志写入、ResWatcher
|
||||
// 事件发布等)。
|
||||
// 4. 事务托底:当 onChange 会被调用、且调用方没有显式传入事务时,自动开一个事务包住这次写操作和
|
||||
// onChange 本身;onChange 失败就整体回滚,成功才提交。调用方显式传入的事务不受影响,提交/
|
||||
// 回滚仍由调用方自己负责。
|
||||
type dataProcessor struct {
|
||||
res *resource
|
||||
u req.User
|
||||
tx *db.TxDatabase
|
||||
ownsTx bool // tx 是否由本次写操作自动开启(而非调用方传入),决定 After 要不要 Commit/Rollback
|
||||
onChange ResChangeRowFunc // 非 nil 表示本次写操作需要抓取快照/收集变更内容,由 Before 阶段的 DataInterceptor 决定
|
||||
rows []changeRow
|
||||
silent bool // ResOptions.Silent:跳过变更通知,见 req.WithSilent 注释
|
||||
skipPermission bool // ResOptions.RolesScope 不是 ResScopeOn/ResScopeAll:跳过权限过滤/字段脱敏,见 req.WithRolesScope 注释
|
||||
}
|
||||
|
||||
// UseTx 在 Before 之前调用:如果本次写操作会触发 onChange、且调用方没有显式传入事务,自动开一个
|
||||
// 事务包住写操作和 onChange。调用方已显式传入的事务(dp.tx != nil)不受影响。
|
||||
func (dp *dataProcessor) UseTx(dataset interface{}) (exec.QueryFactory, error) {
|
||||
if dp.res.IsVirtual() || dp.tx != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var category ResEventType
|
||||
switch dataset.(type) {
|
||||
case *db.InsertDataset:
|
||||
category = ResEventInsert
|
||||
case *db.UpdateDataset:
|
||||
category = ResEventUpdate
|
||||
case *db.DeleteDataset:
|
||||
category = ResEventDelete
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
dp.applyIntercept(category)
|
||||
|
||||
if dp.onChange == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tx, err := dp.res.DB().Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dp.tx = tx
|
||||
dp.ownsTx = true
|
||||
|
||||
return exec.NewQueryFactory(tx), nil
|
||||
}
|
||||
|
||||
// applyIntercept 调用容器里注册的 DataInterceptor,返回角色权限过滤条件,并记录本次写完后要不要调用、调用谁。
|
||||
// skipPermission 时丢弃行级权限过滤条件;silent 时不设置 onChange(写完后不会触发变更通知)。两者
|
||||
// 相互独立,可以只生效一个。
|
||||
func (dp *dataProcessor) applyIntercept(category ResEventType) (sub *db.SelectDataset, cond db.Expression) {
|
||||
sub, cond, onChange := dp.res.intercept(dp.u, category)
|
||||
if dp.skipPermission {
|
||||
sub, cond = nil, nil
|
||||
}
|
||||
if !dp.silent {
|
||||
dp.onChange = onChange
|
||||
}
|
||||
return sub, cond
|
||||
}
|
||||
|
||||
// captureSnapshot 在写操作执行前,查出受影响的行存入 rows
|
||||
func (dp *dataProcessor) captureSnapshot(where exp.ExpressionList) {
|
||||
sd := dp.res.GetDBTable(dp.u)
|
||||
if where != nil && len(where.Expressions()) > 0 {
|
||||
sd = sd.Where(where.Expressions()...)
|
||||
}
|
||||
snaps, err := sd.Executor().GetRecords()
|
||||
if err != nil {
|
||||
log.Printf("captureSnapshot err: %v", err)
|
||||
log.Println(sd.Executor().ToSQL())
|
||||
return
|
||||
}
|
||||
dp.rows = make([]changeRow, len(snaps))
|
||||
for i, snap := range snaps {
|
||||
dp.rows[i].snapshot = snap
|
||||
if id, ok := snap["id"]; ok {
|
||||
dp.rows[i].id = cast.ToInt64(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// insertRowIds 计算本次 INSERT 各行的自增 id(MySQL: lastId 是第一条,SQLite: lastId 是最后一条)
|
||||
func (dp *dataProcessor) insertRowIds(result sql.Result) []int64 {
|
||||
lastId, _ := result.LastInsertId()
|
||||
count, _ := result.RowsAffected()
|
||||
|
||||
ids := make([]int64, count)
|
||||
for i := int64(0); i < count; i++ {
|
||||
rowId := lastId + i
|
||||
if isLocalDB(dp.res.DB().Dialect()) {
|
||||
rowId = lastId - count + 1 + i
|
||||
}
|
||||
ids[i] = rowId
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// finalizeInsertedRows 把计算好的自增 id 写回每一行;INSERT ... SELECT 场景 Before 阶段拿不到具体数据,
|
||||
// 这里改为按 id 反查补全(见 fetchInsertedRows)
|
||||
func (dp *dataProcessor) finalizeInsertedRows(result sql.Result) {
|
||||
ids := dp.insertRowIds(result)
|
||||
|
||||
if len(dp.rows) == 0 {
|
||||
dp.fetchInsertedRows(ids)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range dp.rows {
|
||||
if i < len(ids) {
|
||||
dp.rows[i].id = ids[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fetchInsertedRows 用于 INSERT ... SELECT 场景:写入前拿不到具体行数据,写入后按 id 反查补全 changes
|
||||
func (dp *dataProcessor) fetchInsertedRows(ids []int64) {
|
||||
recs, err := dp.res.GetDBTable(dp.u).Where(db.C("id").In(ids)).Executor().GetRecords()
|
||||
if err != nil {
|
||||
log.Printf("fetchInsertedRows err: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
byId := make(map[int64]map[string]any, len(recs))
|
||||
for _, rec := range recs {
|
||||
if id, ok := rec["id"]; ok {
|
||||
byId[cast.ToInt64(id)] = rec
|
||||
}
|
||||
}
|
||||
|
||||
dp.rows = make([]changeRow, len(ids))
|
||||
for i, id := range ids {
|
||||
dp.rows[i].id = id
|
||||
if rec, ok := byId[id]; ok {
|
||||
dp.rows[i].changes = dp.changesFromNewRow(rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildChanges 将本次 UPDATE 实际写入的字段值记入每一行的 changes
|
||||
func (dp *dataProcessor) buildChanges(newValues db.Record) {
|
||||
if len(dp.rows) == 0 || len(newValues) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
resolved := make(map[string]any, len(newValues))
|
||||
for k, v := range newValues {
|
||||
resolved[k] = dp.resolveLiteralValue(v)
|
||||
}
|
||||
|
||||
for i := range dp.rows {
|
||||
dp.rows[i].changes = resolved
|
||||
}
|
||||
}
|
||||
|
||||
// resolveLiteralValue 从 db.V/db.L 构造的字面量表达式中还原真实值,用于变更对比与序列化
|
||||
func (dp *dataProcessor) resolveLiteralValue(v any) any {
|
||||
lit, ok := v.(exp.LiteralExpression)
|
||||
if !ok {
|
||||
return v
|
||||
}
|
||||
if lit.Literal() == "?" {
|
||||
if args := lit.Args(); len(args) == 1 {
|
||||
return args[0]
|
||||
}
|
||||
}
|
||||
return lit.Literal()
|
||||
}
|
||||
|
||||
// changesFromNewRow 还原 INSERT 行的最终保存值,作为 changes 直接记录
|
||||
func (dp *dataProcessor) changesFromNewRow(row map[string]any) map[string]any {
|
||||
data := make(map[string]any, len(row))
|
||||
for k, v := range row {
|
||||
data[k] = dp.resolveLiteralValue(v)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) Before(dataset interface{}) *db.HookError {
|
||||
if err := dp.before(dataset); err != nil {
|
||||
return &db.HookError{Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) before(dataset interface{}) error {
|
||||
switch d := dataset.(type) {
|
||||
case *db.SelectDataset:
|
||||
return dp.beforeSelectDataset(d)
|
||||
case *db.InsertDataset:
|
||||
return dp.beforeInsertDataset(d)
|
||||
case *db.UpdateDataset:
|
||||
return dp.beforeUpdateDataset(d)
|
||||
case *db.DeleteDataset:
|
||||
return dp.beforeDeleteDataset(d)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) After(dataset interface{}, result interface{}) *db.HookError {
|
||||
if err := dp.after(dataset, result); err != nil {
|
||||
return &db.HookError{Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) after(dataset interface{}, result interface{}) error {
|
||||
if dp.res.IsVirtual() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var category ResEventType
|
||||
switch dataset.(type) {
|
||||
case *db.SelectDataset:
|
||||
return nil
|
||||
case *db.InsertDataset:
|
||||
category = ResEventInsert
|
||||
case *db.UpdateDataset:
|
||||
category = ResEventUpdate
|
||||
case *db.DeleteDataset:
|
||||
category = ResEventDelete
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
r := result.(sql.Result)
|
||||
|
||||
if !dp.silent {
|
||||
if err := dp.res.changeEvent(NewResChangeEvent(category, dp.res, dp.u, r)); err != nil {
|
||||
log.Printf("ResChangeEventFunc err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ResChangeRowFunc 由 Before 阶段 DataInterceptor 返回的 onChange 直接提供,非 nil 才调用
|
||||
var hookErr error
|
||||
if dp.onChange != nil {
|
||||
if category == ResEventInsert {
|
||||
dp.finalizeInsertedRows(r)
|
||||
}
|
||||
rows := make([]ResChangeRow, len(dp.rows))
|
||||
for i, row := range dp.rows {
|
||||
rows[i] = NewResChangeRow(category, dp.res, dp.u, row.id, WithOld(row.snapshot), WithNew(row.changes), WithTx(dp.tx))
|
||||
}
|
||||
hookErr = dp.onChange(rows)
|
||||
}
|
||||
|
||||
if dp.ownsTx {
|
||||
if hookErr != nil {
|
||||
if rbErr := dp.tx.Rollback(); rbErr != nil {
|
||||
log.Printf("dataProcessor tx rollback err: %v", rbErr)
|
||||
}
|
||||
} else if cmErr := dp.tx.Commit(); cmErr != nil {
|
||||
hookErr = cmErr
|
||||
}
|
||||
}
|
||||
|
||||
return hookErr
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) beforeInsertDataset(id *db.InsertDataset) error {
|
||||
// INSERT 不涉及权限过滤(新建的行无需按角色可见性过滤),只关心是否需要抓取变更内容
|
||||
dp.applyIntercept(ResEventInsert)
|
||||
|
||||
switch true {
|
||||
case id.GetClauses().HasRows():
|
||||
return dp.beforeInsertRows(id)
|
||||
case id.GetClauses().HasVals():
|
||||
return dp.beforeInsertColsVals(id)
|
||||
case id.GetClauses().HasFrom():
|
||||
return dp.beforeInsertFromQuery(id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) beforeInsertRows(id *db.InsertDataset) error {
|
||||
rows := id.GetClauses().Rows()
|
||||
|
||||
for i := range rows {
|
||||
rowValue := reflect.ValueOf(rows[i])
|
||||
if rowValue.Kind() == reflect.Ptr {
|
||||
rowValue = rowValue.Elem()
|
||||
}
|
||||
if rowValue.Kind() == reflect.Struct {
|
||||
if row, err := exp.NewRecordFromStruct(rowValue.Interface(), true, false); err != nil {
|
||||
return err
|
||||
} else {
|
||||
rows[i] = row
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range rows {
|
||||
row, ok := rows[i].(map[string]any)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("row is %T, want map[string]any", row)
|
||||
}
|
||||
|
||||
if err := dp.normalizeSaveValue(row); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dp.applyDefaultValue(row, true, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dp.onChange != nil {
|
||||
dp.rows = append(dp.rows, changeRow{changes: dp.changesFromNewRow(row)})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) beforeInsertColsVals(id *db.InsertDataset) error {
|
||||
cols := id.GetClauses().Cols()
|
||||
vals := id.GetClauses().Vals()
|
||||
|
||||
colsName := []string{}
|
||||
for _, col := range cols.Columns() {
|
||||
colsName = append(colsName, col.(exp.IdentifierExpression).GetCol().(string))
|
||||
}
|
||||
|
||||
extraColsName := []string{}
|
||||
colsVal := []any{}
|
||||
for k, v := range lo.OmitByKeys(dp.getFieldsDefaultValue(true, false), colsName) {
|
||||
cols = cols.Append(db.C(k))
|
||||
extraColsName = append(extraColsName, k)
|
||||
colsVal = append(colsVal, v)
|
||||
}
|
||||
allColsName := append(append([]string{}, colsName...), extraColsName...)
|
||||
|
||||
for i := 0; i < len(vals); i++ {
|
||||
for j := 0; j < len(vals[i]); j++ {
|
||||
if filed, ok := dp.res.GetField(colsName[j]); ok {
|
||||
vals[i][j] = filed.ToValue(vals[i][j])
|
||||
}
|
||||
}
|
||||
vals[i] = append(vals[i], colsVal...)
|
||||
|
||||
if dp.onChange != nil {
|
||||
row := make(map[string]any, len(allColsName))
|
||||
for j, name := range allColsName {
|
||||
if j < len(vals[i]) {
|
||||
row[name] = vals[i][j]
|
||||
}
|
||||
}
|
||||
dp.rows = append(dp.rows, changeRow{changes: dp.changesFromNewRow(row)})
|
||||
}
|
||||
}
|
||||
|
||||
*id = *id.Cols(cols)
|
||||
return nil
|
||||
}
|
||||
|
||||
// beforeInsertFromQuery INSERT ... SELECT:写入内容来自子查询,写入前无法拿到具体行数据,
|
||||
// changes 由 After 在写入完成后按 id 反查补全(见 fetchInsertedRows)
|
||||
func (dp *dataProcessor) beforeInsertFromQuery(id *db.InsertDataset) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeSaveValue 规范化保存数据:
|
||||
// 1. 移除系统缺省字段, 如果是 insert 需要调用 applyDefaultValue
|
||||
// 2. 移除系统中没有的字段
|
||||
// 3. 移除当前用户没有权限写的字段(ResField.GetRoles,语义同查询脱敏,见 hasFieldAccess)——
|
||||
// 静默丢弃而不是报错,跟"系统中没有的字段"一致:数据库里对应列保持原值不变。skipPermission
|
||||
// 时跳过这条检查,字段规范化/类型转换本身不受影响。
|
||||
func (dp *dataProcessor) normalizeSaveValue(row db.Record) error {
|
||||
for k, v := range row {
|
||||
if k == "id" || k == "created_user" || k == "created_at" || k == "deleted_at" || k == "updated_at" {
|
||||
delete(row, k)
|
||||
continue
|
||||
}
|
||||
|
||||
field, hasField := dp.res.GetField(k)
|
||||
if hasField && !dp.skipPermission && !hasFieldAccess(field, dp.u) {
|
||||
delete(row, k)
|
||||
continue
|
||||
}
|
||||
|
||||
if val, ok := v.(db.Expression); ok {
|
||||
row[k] = val
|
||||
} else if hasField {
|
||||
row[k] = field.ToValue(v)
|
||||
} else {
|
||||
delete(row, k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyDefaultValue 填充默认数据
|
||||
func (dp *dataProcessor) applyDefaultValue(row db.Record, forInsert, forUpdate bool) error {
|
||||
for k, v := range dp.getFieldsDefaultValue(forInsert, forUpdate) {
|
||||
if _, ok := row[k]; !ok {
|
||||
row[k] = v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) getFieldsDefaultValue(forInsert, forUpdate bool) map[string]db.Expression {
|
||||
defs := map[string]db.Expression{}
|
||||
|
||||
if forInsert {
|
||||
for _, item := range dp.res.fields {
|
||||
if item.GetCode() == "updated_at" || item.GetCode() == "created_user" || item.GetCode() == "owned_user" {
|
||||
continue
|
||||
}
|
||||
raw := item.GetRawDefault()
|
||||
if raw != nil {
|
||||
if lit, ok := raw.(exp.LiteralExpression); ok && strings.HasPrefix(strings.ToLower(lit.Literal()), "sql:") {
|
||||
defs[item.GetCode()] = raw
|
||||
} else if item.GetDataType() == req.ResJson {
|
||||
defs[item.GetCode()] = raw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := defs["owned_user"]; !ok {
|
||||
defs["owned_user"] = db.V(dp.u.Uuid())
|
||||
}
|
||||
defs["created_user"] = db.V(dp.u.Uuid())
|
||||
}
|
||||
|
||||
if forUpdate && isLocalDB(dp.res.DB().Dialect()) {
|
||||
defs["updated_at"] = db.L("CURRENT_TIMESTAMP")
|
||||
}
|
||||
|
||||
return defs
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) beforeSelectDataset(sd *db.SelectDataset) error {
|
||||
sub, ex := dp.applyIntercept(ResEventSelect)
|
||||
if ex != nil {
|
||||
*sd = *sd.Where(ex)
|
||||
} else if sub != nil {
|
||||
table := sd.GetClauses().From().Columns()[0]
|
||||
if alias, ok := table.(exp.AliasedExpression); ok {
|
||||
*sd = *sd.From(db.V(sub.Expression()).As(alias.GetAs()))
|
||||
} else {
|
||||
*sd = *sd.From(db.V(sub.Expression()).As(table))
|
||||
}
|
||||
}
|
||||
return dp.maskSelect(sd)
|
||||
}
|
||||
|
||||
// maskSelect 按字段级权限(ResField.GetRoles)替换 SELECT 列表里当前用户没有权限查看的字段:
|
||||
// - 没有显式选择过列(裸的 db.Star()),或者显式选了限定到当前资源自己别名的 db.T(alias).All()
|
||||
// ——两种写法效果一样,都是"选当前资源自己的全部列":展开成资源自己的全部字段,逐个按权限
|
||||
// 判断要不要替换成脱敏值。只覆盖当前资源自己的字段,JOIN 进来的字段没有被展开进来,覆盖不到。
|
||||
// - 其它情况下显式选择了列(通常来自 orm-v2 QueryField.ToSql()):只处理其中被 MaskField(...)
|
||||
// 显式标记过的列,未标记的列一律原样保留——标记是显式的,不去猜哪一列对应哪个字段。这时候才
|
||||
// 真正解析 MaskField 标记的别名对应哪个资源、哪个字段(见 resolveLazyMask),解析失败说明
|
||||
// 调用方标记的表名/字段名写错了,是编程错误,直接报错而不是静默跳过。
|
||||
func (dp *dataProcessor) maskSelect(sd *db.SelectDataset) error {
|
||||
if dp.res.IsVirtual() || dp.skipPermission {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !dp.hasRestrictedFields(sd) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if dp.isDefaultSelect(sd) {
|
||||
if len(dp.res.fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
cols := make([]interface{}, len(dp.res.fields))
|
||||
changed := false
|
||||
for i, field := range dp.res.fields {
|
||||
if hasFieldAccess(field, dp.u) {
|
||||
cols[i] = db.C(field.GetCode())
|
||||
} else {
|
||||
cols[i] = dp.res.mask(field).As(field.GetCode())
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
*sd = *sd.Select(cols...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
old := sd.GetClauses().Select().Columns()
|
||||
cols := make([]interface{}, len(old))
|
||||
changed := false
|
||||
for i, c := range old {
|
||||
resolved, colChanged, err := dp.resolveMaskedExpr(sd, c, true)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cols[i] = resolved
|
||||
if colChanged {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
*sd = *sd.Select(cols...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasRestrictedFields 判断这次查询可能碰到的字段——当前资源自己的,加上 FROM/JOIN 子句里所有
|
||||
// 能解析到具体资源的——里有没有任何一个配置了 Roles。一个都没有的话,maskSelect 后面所有的工作
|
||||
// (SELECT * 展开、显式 MaskField 列解析)都不会有任何效果,可以在最前面直接跳过整个函数,不用
|
||||
// 再管是不是默认 SELECT、SELECT 里有没有标记过的列。
|
||||
//
|
||||
// JOIN 的表如果解析不到具体资源(不是这套资源系统管理的表、容器没注册 req.GetResource、或者
|
||||
// 这张表在 FROM/JOIN 里的写法认不出真实表名),不能当成"没有受限字段"直接放过——万一这张表上
|
||||
// 真的有 MaskField(...) 标记,那本该在 resolveLazyField 里报错(表名/字段名配置错误),提前跳过
|
||||
// 就变成静默放过,比现在的报错行为更危险。这些拿不准的情况一律返回 true(不跳过),退回到老路径
|
||||
// 让后面该报错报错、该处理处理。
|
||||
//
|
||||
// 有个前提:这里只看 sd 真实的 FROM/JOIN 子句。MaskField("alias.col") 里 alias 指向一个压根没
|
||||
// JOIN 进这次查询的资源时(resolveLazyField 会把 alias 当 code/table/uuid 兜底直接查一次),这
|
||||
// 里扫不到那个资源,如果它自己没有受限字段又恰好没被扫进来,会被误判成"整个查询不用脱敏"直接跳过
|
||||
// ——这种查询本来就没有实际意义(列不属于 FROM 里的任何一张表,真跑数据库会报错),MaskField 的
|
||||
// alias 必须对应查询里真实 JOIN 过的表,是这个快速跳过路径的前提。
|
||||
//
|
||||
// 超级管理员(HasUserRoles() 不带参数调用,只命中超管角色那一支)对任何字段都有权限,不管查了
|
||||
// 哪些表、字段配置了什么 Roles,脱敏最终都不会生效(hasFieldAccess 对超管永远返回 true)——直接
|
||||
// 跳过整个解析过程,既是性能优化,也避免了系统自己在资源初始化阶段用系统用户递归发起查询时,
|
||||
// req.GetResource 还没注册好导致的 DI 循环依赖报错(跟 framework-v2 的 res_scope.go 里
|
||||
// getResUserRolesCondition 对同类问题的处理是一个思路)。
|
||||
func (dp *dataProcessor) hasRestrictedFields(sd *db.SelectDataset) bool {
|
||||
if dp.u.HasUserRoles() {
|
||||
return false
|
||||
}
|
||||
|
||||
if dp.fieldsHaveRoles(dp.res.fields) {
|
||||
return true
|
||||
}
|
||||
|
||||
getResource, err := do.Invoke[req.GetResource](dp.res.container)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
clauses := sd.GetClauses()
|
||||
tables := make([]exp.Expression, 0, 1+len(clauses.Joins()))
|
||||
if from := clauses.From().Columns(); len(from) > 0 {
|
||||
tables = append(tables, from[0])
|
||||
}
|
||||
for _, j := range clauses.Joins() {
|
||||
tables = append(tables, j.Table())
|
||||
}
|
||||
|
||||
for _, t := range tables {
|
||||
aliased, ok := t.(exp.AliasedExpression)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
table, ok := dp.realTableOf(aliased.Aliased())
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
res, ok := getResource(table)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if dp.fieldsHaveRoles(res.GetFields()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// fieldsHaveRoles 判断 fields 里有没有任何一个配置了 Roles(hasRestrictedFields 对当前资源自己
|
||||
// 和每个能解析到的 JOIN 资源都要做一次这个判断)
|
||||
func (dp *dataProcessor) fieldsHaveRoles(fields []req.ResField) bool {
|
||||
for _, field := range fields {
|
||||
if len(field.GetRoles()) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolveMaskedExpr 递归处理 c 内部被 MaskField 标记的列,包括嵌套在 db.Func(...) 参数里的情况
|
||||
// (比如 db.Func("CONCAT", resx.MaskField("t.a"), resx.MaskField("t.b")))——顶层 SELECT 列表
|
||||
// 只按类型断言直接标记的列,标记被函数包一层之后顶层类型就变成了 SQLFunctionExpression,断言不
|
||||
// 出来,得递归进 Args() 里找。
|
||||
//
|
||||
// topLevel 为 true 表示 c 本身就是一整个 SELECT 列,这时候没权限要换成 "mask值 AS 别名";
|
||||
// topLevel 为 false 表示 c 是嵌在别的表达式(目前只处理 db.Func 的参数)里面,这时候只能换成
|
||||
// 裸的 mask 值,不能带 AS——SQL 函数参数没法单独起别名。
|
||||
//
|
||||
// 目前只递归处理 SQLFunctionExpression(db.Func(...))和包着它的 AliasedExpression
|
||||
// (db.Func(...).As(...)):这是这个代码库里唯一会把 MaskField 包起来用的场景(聚合/计算字段),
|
||||
// 没有覆盖 CastExpression/CaseExpression/WindowExpression 等目前用不到的包装类型。
|
||||
func (dp *dataProcessor) resolveMaskedExpr(sd *db.SelectDataset, c interface{}, topLevel bool) (interface{}, bool, error) {
|
||||
if r, ok := c.(lazyMaskResolver); ok {
|
||||
field, unresolved, err := dp.resolveLazyField(sd, r)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if unresolved {
|
||||
// req.GetResource 本身解析失败(见 resolveLazyField 的说明),没有具体字段可参考
|
||||
// 数据类型,退到 db.V(nil) 兜底——跟 defaultMask 认不出数据类型时的兜底分支一致。
|
||||
if !topLevel {
|
||||
return db.V(nil), true, nil
|
||||
}
|
||||
return db.V(nil).As(dp.fallbackAlias(c, r)), true, nil
|
||||
}
|
||||
|
||||
if hasFieldAccess(field, dp.u) {
|
||||
return c, false, nil
|
||||
}
|
||||
if !topLevel {
|
||||
return dp.res.mask(field), true, nil
|
||||
}
|
||||
return dp.res.mask(field).As(dp.fallbackAlias(c, r)), true, nil
|
||||
}
|
||||
|
||||
if fn, ok := c.(exp.SQLFunctionExpression); ok {
|
||||
args := fn.Args()
|
||||
newArgs := make([]interface{}, len(args))
|
||||
changed := false
|
||||
for i, a := range args {
|
||||
resolved, argChanged, err := dp.resolveMaskedExpr(sd, a, false)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if argChanged {
|
||||
changed = true
|
||||
newArgs[i] = resolved
|
||||
} else {
|
||||
newArgs[i] = a
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return c, false, nil
|
||||
}
|
||||
return db.Func(fn.Name(), newArgs...), true, nil
|
||||
}
|
||||
|
||||
if aliased, ok := c.(exp.AliasedExpression); ok {
|
||||
inner, innerChanged, err := dp.resolveMaskedExpr(sd, aliased.Aliased(), false)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !innerChanged {
|
||||
return c, false, nil
|
||||
}
|
||||
return inner.(exp.Aliaseable).As(aliased.GetAs()), true, nil
|
||||
}
|
||||
|
||||
return c, false, nil
|
||||
}
|
||||
|
||||
// fallbackAlias 在拿不到真正的 req.ResField(没法用 field.GetCode())时,给替换后的表达式起个
|
||||
// 别名:优先用调用方自己指定的别名(MaskField(...).As("xxx")),没有的话退到列名本身
|
||||
// (lazyMaskField() 的第二个返回值)。
|
||||
func (dp *dataProcessor) fallbackAlias(c interface{}, r lazyMaskResolver) string {
|
||||
if aliased, ok := c.(exp.AliasedExpression); ok {
|
||||
if col, ok := aliased.GetAs().GetCol().(string); ok && col != "" {
|
||||
return col
|
||||
}
|
||||
}
|
||||
_, code := r.lazyMaskField()
|
||||
return code
|
||||
}
|
||||
|
||||
// resolveLazyField 把 MaskField(...) 标记的"待解析"列解析成真正的 req.ResField:codeResource
|
||||
// 为空表示当前资源自己的字段,直接用 dp.res;否则把 codeResource 当 SQL 别名,在 sd 的 FROM/JOIN
|
||||
// 子句里换成真实表名(见 resolveJoinTable),换不到就把它本身当 code/table/uuid 兜底试一次,再用
|
||||
// req.GetResource 查资源。
|
||||
// unresolved=true 表示 req.GetResource 本身解析失败——常见原因是资源系统自己还在初始化过程中
|
||||
// 递归触发了一次带 JOIN 的查询,形成真正的循环依赖(不是调用方标记写错了表名/字段名,那种情况
|
||||
// 会走下面别的报错分支)。这种情况不能让整个查询因为一次 DI 时序问题报错崩掉:调用方
|
||||
// resolveMaskedExpr 拿到 unresolved=true 后会保守地当成需要脱敏处理,宁可多脱敏一列,也不能
|
||||
// 打崩查询。
|
||||
func (dp *dataProcessor) resolveLazyField(sd *db.SelectDataset, r lazyMaskResolver) (field req.ResField, unresolved bool, err error) {
|
||||
codeResource, code := r.lazyMaskField()
|
||||
|
||||
var res req.Resource
|
||||
if codeResource == "" || codeResource == dp.res.GetCode() || codeResource == dp.fromAlias(sd) {
|
||||
// 裸列名,或者 codeResource 就是当前资源自己(不管是 dp.res 自己的 code,还是这次查询给它
|
||||
// 起的 FROM 别名):这两种情况都不是真的 JOIN 进来的字段,直接用 dp.res,完全不用碰
|
||||
// req.GetResource——DataListField 之类的配置记录里,CodeResource 对"master/自身资源"的
|
||||
// 字段本来就常常直接填资源自己的 code,不能一律当成外部资源硬查 DI,否则会把当前资源自己
|
||||
// 的所有字段都误判成"需要走 JOIN 解析",一旦 req.GetResource 暂时不可用,会把整行字段
|
||||
// 全部误脱敏,而不是只脱敏真的配置了 Roles 的那几个。
|
||||
res = dp.res
|
||||
} else {
|
||||
// 到这里 codeResource 已经不是资源自己的标识了,是这次查询里用的 SQL 别名,去 FROM/JOIN
|
||||
// 子句里当别名搜——只有这个语境下才适合叫 alias。
|
||||
alias := codeResource
|
||||
table, ok := dp.resolveJoinTable(sd, alias)
|
||||
if !ok {
|
||||
table = alias
|
||||
}
|
||||
|
||||
getResource, err := do.Invoke[req.GetResource](dp.res.container)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
found, ok := getResource(table)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("resx: MaskField 标记的列 %q 找不到对应的资源(别名/表名 %q)", code, table)
|
||||
}
|
||||
res = found
|
||||
}
|
||||
|
||||
field, ok := res.GetField(code)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("resx: 资源 %q 没有字段 %q", res.GetCode(), code)
|
||||
}
|
||||
|
||||
return field, false, nil
|
||||
}
|
||||
|
||||
// resolveJoinTable 在 sd 的 FROM/JOIN 子句里找别名是 alias 的那一个,返回它的真实表名。
|
||||
// MaskField("alias.col") 里的 alias 通常是资源自己的 code(约定,但不保证——比如同一个资源自
|
||||
// 连接时两个别名不可能都等于资源 code),只有找到真实表名,才能用 req.GetResource 的 table 匹配
|
||||
// 稳定查到资源,不用赌 alias 本身就是 code。
|
||||
func (dp *dataProcessor) resolveJoinTable(sd *db.SelectDataset, alias string) (string, bool) {
|
||||
clauses := sd.GetClauses()
|
||||
|
||||
if from := clauses.From().Columns(); len(from) > 0 {
|
||||
if t, ok := dp.aliasedRealTable(from[0], alias); ok {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
|
||||
for _, j := range clauses.Joins() {
|
||||
if t, ok := dp.aliasedRealTable(j.Table(), alias); ok {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// aliasIdentOf 从 AliasedExpression 里取出别名字符串,兼容两种起别名的路径(同 fromAlias 的说明:
|
||||
// 落在 GetAs() 的 Table 字段还是 Col 字段,取决于当初是直接在 identifier 上调 As(),还是像
|
||||
// GetTableExpr() 那样先包一层字面量再 As())。
|
||||
func (dp *dataProcessor) aliasIdentOf(as exp.IdentifierExpression) string {
|
||||
if t := as.GetTable(); t != "" {
|
||||
return t
|
||||
}
|
||||
if c, ok := as.GetCol().(string); ok {
|
||||
return c
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// aliasedRealTable 如果 e 是别名为 alias 的表引用,返回它包着的真实表名。
|
||||
func (dp *dataProcessor) aliasedRealTable(e exp.Expression, alias string) (string, bool) {
|
||||
aliased, ok := e.(exp.AliasedExpression)
|
||||
if !ok || dp.aliasIdentOf(aliased.GetAs()) != alias {
|
||||
return "", false
|
||||
}
|
||||
return dp.realTableOf(aliased.Aliased())
|
||||
}
|
||||
|
||||
// realTableOf 从"被起别名的那个表达式"里取出真实表名,不管别名是什么。兼容两种构造路径:
|
||||
// db.T(table).As(alias) 时 Aliased() 直接就是原始 identifier;res.GetTableExpr().As(alias)
|
||||
// (GetTableExpr 用 db.V(db.T(table)) 包了一层字面量)时 Aliased() 是字面量,真实的 identifier
|
||||
// 在它的 Args()[0] 里。
|
||||
func (dp *dataProcessor) realTableOf(e exp.Expression) (string, bool) {
|
||||
switch v := e.(type) {
|
||||
case exp.IdentifierExpression:
|
||||
return v.GetTable(), true
|
||||
case exp.LiteralExpression:
|
||||
args := v.Args()
|
||||
if len(args) != 1 {
|
||||
return "", false
|
||||
}
|
||||
if ident, ok := args[0].(exp.IdentifierExpression); ok {
|
||||
return ident.GetTable(), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// isDefaultSelect 判断 SELECT 列表是不是"选当前资源自己的全部列":要么是裸的 db.Star()
|
||||
// (clauses.IsDefaultSelect()),要么是显式限定到当前资源自己别名的 db.T(alias).All()——两种
|
||||
// 写法效果一样,都应该走同一套"展开成显式字段列表"的逻辑。限定到别的(比如 JOIN 进来的)别名
|
||||
// 的 T().All() 不算,那是另一个资源的全部列,这里够不到。
|
||||
func (dp *dataProcessor) isDefaultSelect(sd *db.SelectDataset) bool {
|
||||
clauses := sd.GetClauses()
|
||||
if clauses.IsDefaultSelect() {
|
||||
return true
|
||||
}
|
||||
|
||||
cols := clauses.Select().Columns()
|
||||
if len(cols) != 1 {
|
||||
return false
|
||||
}
|
||||
ident, ok := cols[0].(exp.IdentifierExpression)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
lit, ok := ident.GetCol().(exp.LiteralExpression)
|
||||
if !ok || lit.Literal() != "*" {
|
||||
return false
|
||||
}
|
||||
return ident.GetTable() == "" || ident.GetTable() == dp.fromAlias(sd)
|
||||
}
|
||||
|
||||
// fromAlias 取出 FROM 子句里当前资源自己的别名(GetDBTable 默认用资源 code 做别名,也可能被
|
||||
// ResOption 的 Alias 覆盖),取不到就返回空字符串。
|
||||
//
|
||||
// 别名字符串具体落在 GetAs() 的 Table 字段还是 Col 字段,取决于当初是怎么 As() 出来的:
|
||||
// GetTableExpr() 内部用 db.V(db.T(table)) 包了一层字面量,literal.As() 走的是
|
||||
// NewAliasExpression -> ParseIdentifier,单个词(不带 ".")会被解析成 col,不是 table;而
|
||||
// JOIN 场景常见的 db.T(code).As(alias) 是直接在 identifier 上调用 As(),走的是另一条路径,
|
||||
// 会把别名存进 table 字段。两种都要认。
|
||||
func (dp *dataProcessor) fromAlias(sd *db.SelectDataset) string {
|
||||
table := sd.GetClauses().From().Columns()[0]
|
||||
aliased, ok := table.(exp.AliasedExpression)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return dp.aliasIdentOf(aliased.GetAs())
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) beforeUpdateDataset(ud *db.UpdateDataset) error {
|
||||
if dp.res.IsVirtual() {
|
||||
return nil
|
||||
}
|
||||
sub, ex := dp.applyIntercept(ResEventUpdate)
|
||||
if ex != nil {
|
||||
*ud = *ud.Where(ex)
|
||||
} else if sub != nil {
|
||||
table := ud.GetClauses().Table()
|
||||
if alias, ok := table.(exp.AliasedExpression); ok {
|
||||
*ud = *ud.Where(alias.GetAs().Col("id").In(sub.Select(db.T(dp.res.GetCode()).Col("id"))))
|
||||
} else {
|
||||
*ud = *ud.Where(db.L("?.id", table).In(sub.Select(db.T(dp.res.GetCode()).Col("id"))))
|
||||
}
|
||||
}
|
||||
|
||||
if dp.onChange != nil {
|
||||
dp.captureSnapshot(ud.GetClauses().Where())
|
||||
}
|
||||
|
||||
if ud.GetClauses().HasSetValues() {
|
||||
udv := ud.GetClauses().SetValues()
|
||||
|
||||
var data db.Record
|
||||
switch v := udv.(type) {
|
||||
case map[string]any:
|
||||
data = v
|
||||
case db.Record:
|
||||
data = v
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %T", udv)
|
||||
}
|
||||
|
||||
if err := dp.normalizeSaveValue(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dp.onChange != nil {
|
||||
dp.buildChanges(data)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) beforeDeleteDataset(dd *db.DeleteDataset) error {
|
||||
if dp.res.IsVirtual() {
|
||||
return nil
|
||||
}
|
||||
sub, ex := dp.applyIntercept(ResEventDelete)
|
||||
if ex != nil {
|
||||
*dd = *dd.Where(ex)
|
||||
} else if sub != nil {
|
||||
table := dd.GetClauses().From().Columns()[0]
|
||||
if alias, ok := table.(exp.AliasedExpression); ok {
|
||||
*dd = *dd.Where(alias.GetAs().Col("id").In(sub.Select(db.T(dp.res.GetCode()).Col("id"))))
|
||||
} else {
|
||||
*dd = *dd.Where(db.L("?.id", table).In(sub.Select(db.T(dp.res.GetCode()).Col("id"))))
|
||||
}
|
||||
}
|
||||
|
||||
if dp.onChange != nil {
|
||||
dp.captureSnapshot(dd.GetClauses().Where())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
// Package resx 提供 req.Resource 接口的实现。
|
||||
//
|
||||
// 角色权限过滤与写操作后的处理(变更留痕、ResWatcher 事件发布等)依赖更上层的 contracts 包
|
||||
// (NewOrm/NewOrmJoin/GetOrmConditions/Queue 等),而 contracts 包本身依赖 req,为避免循环
|
||||
// import,这几块能力通过 DataInterceptor/ResChangeEventFunc/ResChangeRowFunc 三个函数类型传入:
|
||||
// 上层(如 framework)用 do.Provide 把它们分别注册到 DI 容器即可获得完整能力;容器里没有注册
|
||||
// 时优雅降级为空操作,因此这个包在测试或不需要这些能力的场景下(如 orm 包的单元测试)也可以
|
||||
// 直接使用。
|
||||
//
|
||||
// 这几个回调是 per-container(per-app)的概念而不是 per-resource 的:回调参数里已经带 res,可以
|
||||
// 按资源分支处理,同一个容器里的所有资源理应共享同一套实现,所以不作为 resource 的字段通过 Option
|
||||
// 逐个设置,而是像 DB() 一样在使用时从 container 里解析(见 (*resource).intercept/changeEvent)。
|
||||
package resx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/db/engine"
|
||||
"git.fsdpf.net/go/db/exp"
|
||||
"git.fsdpf.net/go/db/schema"
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
// DataInterceptor 每次读写操作前调用。由上层用 do.Provide 注册到 DI 容器;容器里没有注册时不过滤、
|
||||
// 不抓取。返回值:
|
||||
// - sub:权限条件的子查询形式,非 nil 时用于重写 FROM(SELECT)或改写为
|
||||
// `id` IN (子查询)(UPDATE/DELETE);与 cond 互斥,两者都为 nil 表示不过滤。例如
|
||||
// SELECT 拼接后为:
|
||||
// SELECT * FROM (SELECT * FROM `users` AS `User` WHERE (`owned_user` = '...')) AS `User`
|
||||
// UPDATE/DELETE 拼接后为:
|
||||
// UPDATE `users` AS `User` SET ... WHERE (`id` IN ((SELECT `User`.`id` FROM `users` AS `User` WHERE (`owned_user` = '...'))))
|
||||
// - cond:权限条件的表达式形式,非 nil 时直接拼进 WHERE;与 sub 互斥。例如拼接后为:
|
||||
// SELECT * FROM `users` AS `User` WHERE (`owned_user` = '...')
|
||||
// - onChange:非 nil 表示本次写操作需要抓取快照/收集变更内容,写完后会调用它(写入变更
|
||||
// 日志、发布 ResWatcher 事件等要用到);nil 表示不需要,也就完全不会有调用。跟
|
||||
// ResChangeEventFunc 不是一回事:ResChangeEventFunc 不看这个返回值,任何写操作后都会
|
||||
// 无条件调用。SELECT 场景不会用到 onChange,可以恒返回 nil
|
||||
type DataInterceptor func(res req.Resource, u req.User, category ResEventType) (sub *db.SelectDataset, cond db.Expression, onChange ResChangeRowFunc)
|
||||
|
||||
// ResChangeEventFunc 写操作完成后无条件调用一次,不受 DataInterceptor 返回的 onChange 影响(典型
|
||||
// 用途:发布粗粒度的"资源发生变更"事件,如清缓存)。event 已经自带 Type()/Res()/User()/
|
||||
// Result(),不需要额外参数。由上层用 do.Provide 注册到 DI 容器;容器里没有注册时不做任何处理。
|
||||
type ResChangeEventFunc func(event ResChangeEvent) error
|
||||
|
||||
// ResChangeRowFunc 写操作完成后调用,由同一次写操作里 DataInterceptor 的 onChange 返回值直接
|
||||
// 提供(典型用途:写入变更日志、发布 ResWatcher 事件),不是从 DI 容器解析的,因此可以按
|
||||
// 资源/类别/用户各返回不同的处理逻辑。rows 里每一行已经自带 Type()/Res()/User()/ID()/Old()/
|
||||
// New()/Cur()/Tx(),不需要额外参数。
|
||||
type ResChangeRowFunc func(rows []ResChangeRow) error
|
||||
|
||||
// MaskFunc 返回某个字段被脱敏后展示的哨兵值(用户没有权限查看 field 真实值时,SELECT 里这个
|
||||
// 字段会换成这个值)。由上层用 do.Provide 注册到 DI 容器;容器里没有注册时按 field.GetDataType()
|
||||
// 走内置默认值:数字类(Integer/SmallInteger/Decimal)给一个明显不合理的负数,字符串类
|
||||
// (String/Text/Enum)给一个"零宽空格-零宽空格"的三字符标记(视觉上只显示一个短横线,但跟真实
|
||||
// 短横线不是同一个字符串),其它类型给 NULL——都是刻意选的、不会跟真实业务数据混淆的哨兵值,
|
||||
// 具体见 defaultMask。
|
||||
type MaskFunc func(field req.ResField) exp.LiteralExpression
|
||||
|
||||
// DefaultMaskInt 数字类字段的默认哨兵值:绝大多数业务字段(数量/价格/年龄/次数等)不可能出现
|
||||
// 这个量级的负数,比 0 更不容易被误当成真实数据
|
||||
var DefaultMaskInt = -999999999
|
||||
|
||||
// DefaultMaskString 字符串类字段的默认哨兵值:零宽空格 + 短横线 + 零宽空格(U+200B, '-', U+200B)。
|
||||
// 用显式字节而不是直接在源码里放不可见字符,方便 diff/grep 时能看清楚具体是什么;视觉上只显示
|
||||
// 一个"-",但实际是 3 个字符,跟调用方自己存的真实短横线字符串区分得开
|
||||
var DefaultMaskString = string([]byte{0xE2, 0x80, 0x8B, '-', 0xE2, 0x80, 0x8B})
|
||||
|
||||
// defaultMask 内置的按数据类型分支的脱敏默认值,MaskFunc 未注册时使用
|
||||
func defaultMask(field req.ResField) exp.LiteralExpression {
|
||||
switch field.GetDataType() {
|
||||
case req.ResInteger, req.ResSmallInteger, req.ResDecimal:
|
||||
return db.V(DefaultMaskInt)
|
||||
case req.ResString, req.ResText, req.ResEnum:
|
||||
return db.V(DefaultMaskString)
|
||||
default:
|
||||
return db.V(nil)
|
||||
}
|
||||
}
|
||||
|
||||
type resource struct {
|
||||
container do.Injector
|
||||
initOnce *sync.Once
|
||||
|
||||
uuid string
|
||||
code string
|
||||
name string
|
||||
description string
|
||||
conn string
|
||||
isResVirtual bool
|
||||
table string
|
||||
primarykey string
|
||||
historyRoles []string
|
||||
fields []req.ResField
|
||||
}
|
||||
|
||||
// intercept 从容器里解析 DataInterceptor 并调用;容器里没有注册时返回 nil, nil, nil(不过滤、不抓取)。
|
||||
func (this *resource) intercept(u req.User, category ResEventType) (sub *db.SelectDataset, cond db.Expression, onChange ResChangeRowFunc) {
|
||||
fn, err := do.Invoke[DataInterceptor](this.container)
|
||||
if err != nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return fn(this, u, category)
|
||||
}
|
||||
|
||||
// changeEvent 从容器里解析 ResChangeEventFunc 并调用;容器里没有注册时不做任何处理。
|
||||
func (this *resource) changeEvent(event ResChangeEvent) error {
|
||||
fn, err := do.Invoke[ResChangeEventFunc](this.container)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return fn(event)
|
||||
}
|
||||
|
||||
// mask 从容器里解析 MaskFunc 并调用;容器里没有注册时走 defaultMask。
|
||||
func (this *resource) mask(field req.ResField) exp.LiteralExpression {
|
||||
fn, err := do.Invoke[MaskFunc](this.container)
|
||||
if err != nil {
|
||||
return defaultMask(field)
|
||||
}
|
||||
return fn(field)
|
||||
}
|
||||
|
||||
// Option 用于设置 New 里非必填的字段
|
||||
type Option func(r *resource)
|
||||
|
||||
// WithUuid 设置资源 uuid,不设置则为空字符串
|
||||
func WithUuid(v string) Option {
|
||||
return func(r *resource) { r.uuid = v }
|
||||
}
|
||||
|
||||
// WithName 设置资源名,不设置则默认与 code 相同
|
||||
func WithName(v string) Option {
|
||||
return func(r *resource) { r.name = v }
|
||||
}
|
||||
|
||||
// WithDescription 设置资源说明,不设置则为空字符串
|
||||
func WithDescription(v string) Option {
|
||||
return func(r *resource) { r.description = v }
|
||||
}
|
||||
|
||||
// WithConn 设置数据库连接名,不设置则为空字符串(引擎按空字符串键查找连接)
|
||||
func WithConn(v string) Option {
|
||||
return func(r *resource) { r.conn = v }
|
||||
}
|
||||
|
||||
// WithVirtual 标记为虚拟资源
|
||||
func WithVirtual(v bool) Option {
|
||||
return func(r *resource) { r.isResVirtual = v }
|
||||
}
|
||||
|
||||
// WithPrimarykey 设置主键字段名,不设置则默认为 "id"
|
||||
func WithPrimarykey(v string) Option {
|
||||
return func(r *resource) { r.primarykey = v }
|
||||
}
|
||||
|
||||
// WithHistoryRoles 设置变更留痕角色列表,不设置则不留痕
|
||||
func WithHistoryRoles(roles ...string) Option {
|
||||
return func(r *resource) { r.historyRoles = roles }
|
||||
}
|
||||
|
||||
// WithFields 设置资源字段,不设置则为空
|
||||
func WithFields(fields ...req.ResField) Option {
|
||||
return func(r *resource) { r.fields = fields }
|
||||
}
|
||||
|
||||
// New 构造一个 req.Resource。code/table 为必填,其余通过 Option 设置。
|
||||
// 读写钩子(DataInterceptor/ResChangeEventFunc/ResChangeRowFunc)不通过 Option 设置,而是由上层用
|
||||
// do.Provide 注册到 container,见包注释。
|
||||
func New(container do.Injector, code, table string, opts ...Option) req.Resource {
|
||||
r := &resource{
|
||||
container: container,
|
||||
uuid: code,
|
||||
code: code,
|
||||
name: code,
|
||||
table: table,
|
||||
primarykey: "id",
|
||||
initOnce: &sync.Once{},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (this *resource) GetUuid() string {
|
||||
return this.uuid
|
||||
}
|
||||
|
||||
func (this *resource) GetCode() string {
|
||||
return this.code
|
||||
}
|
||||
|
||||
func (this *resource) GetName() string {
|
||||
return this.name
|
||||
}
|
||||
|
||||
func (this *resource) GetDescription() string {
|
||||
return this.description
|
||||
}
|
||||
|
||||
func (this *resource) GetConn() string {
|
||||
return this.conn
|
||||
}
|
||||
|
||||
func (this *resource) GetPrimarykey() string {
|
||||
return this.primarykey
|
||||
}
|
||||
|
||||
func (this *resource) GetHistoryRoles() []string {
|
||||
return this.historyRoles
|
||||
}
|
||||
|
||||
func (this *resource) IsVirtual() bool {
|
||||
return this.isResVirtual
|
||||
}
|
||||
|
||||
func (this *resource) IsSystem() bool {
|
||||
return this.conn == "service-support"
|
||||
}
|
||||
|
||||
func (this *resource) GetFields() []req.ResField {
|
||||
return this.fields
|
||||
}
|
||||
|
||||
func (this *resource) GetField(code string) (req.ResField, bool) {
|
||||
return lo.Find(this.fields, func(v req.ResField) bool {
|
||||
return v.GetCode() == code
|
||||
})
|
||||
}
|
||||
|
||||
func (this *resource) HasField(code string) bool {
|
||||
return lo.SomeBy(this.fields, func(v req.ResField) bool {
|
||||
return v.GetCode() == code
|
||||
})
|
||||
}
|
||||
|
||||
func (this *resource) BeginTransaction() (*db.TxDatabase, error) {
|
||||
return this.DB().Begin()
|
||||
}
|
||||
|
||||
func (this *resource) DB() *db.Database {
|
||||
dbEngine := do.MustInvoke[engine.Engine](this.container)
|
||||
conn := dbEngine.Connection(this.conn)
|
||||
|
||||
if isLocalDB(conn.Dialect()) && !this.isResVirtual && this.table != "" && this.initOnce != nil {
|
||||
this.initOnce.Do(func() {
|
||||
if err := this.autoCreateTable(conn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// vtable 资源:查询走 engine 中的 :memory: 连接("__"+Conn),
|
||||
// 原文件连接仅用于 _vtab_cache 持久化,两者互不阻塞。
|
||||
if conn.Dialect() == "vtable" && this.table != "" && this.initOnce != nil {
|
||||
memConn := dbEngine.Connection("__" + this.conn)
|
||||
this.initOnce.Do(func() {
|
||||
if err := this.autoSetupVtab(memConn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
return memConn
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
// autoSetupVtab 执行 CREATE VIRTUAL TABLE;对应的 vtab 模块由上层通过 req.ResVirtualTableSetup
|
||||
// 提前注册完成(在资源缓存构建时对所有 vtable 连接的资源整体调用一遍,而不是在这里 lazy 触发)
|
||||
func (this *resource) autoSetupVtab(conn *db.Database) error {
|
||||
_, err := conn.Exec(fmt.Sprintf(`CREATE VIRTUAL TABLE IF NOT EXISTS %s USING %s_mod()`, this.table, this.table))
|
||||
return err
|
||||
}
|
||||
|
||||
// autoCreateTable 自动创建数据表
|
||||
func (this *resource) autoCreateTable(conn *db.Database) error {
|
||||
sb := schema.New(conn)
|
||||
return sb.Create(this.table, func(bp *schema.Blueprint) {
|
||||
bp.Comment = this.name
|
||||
for _, field := range this.fields {
|
||||
field.ToBlueprint(bp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// GetTable 原始表名
|
||||
func (this *resource) GetTable() string {
|
||||
return this.table
|
||||
}
|
||||
|
||||
// GetTableExpr 获取资源对应的可用于 FROM/JOIN 的表引用表达式
|
||||
func (this *resource) GetTableExpr() exp.LiteralExpression {
|
||||
if this.isResVirtual {
|
||||
return db.L("(" + this.table + ")")
|
||||
}
|
||||
return db.V(db.T(this.table))
|
||||
}
|
||||
|
||||
// GetDBTable 获取资源对应的数据库连接
|
||||
func (this *resource) GetDBTable(u req.User, opts ...req.ResOption) (sd *db.SelectDataset) {
|
||||
if u == nil {
|
||||
panic("GetDBTable: user cannot be nil")
|
||||
}
|
||||
|
||||
op := &req.ResOptions{}
|
||||
for _, cb := range opts {
|
||||
cb(op)
|
||||
}
|
||||
|
||||
alias := this.GetCode()
|
||||
if op.Alias != "" {
|
||||
alias = op.Alias
|
||||
}
|
||||
|
||||
sd = this.DB().From(this.GetTableExpr().As(alias))
|
||||
|
||||
if op.Tx != nil {
|
||||
sd = op.Tx.From(this.GetTableExpr().As(alias))
|
||||
}
|
||||
|
||||
skipPermission := op.RolesScope != req.ResScopeOn && op.RolesScope != req.ResScopeAll
|
||||
sd.WithHook(&dataProcessor{res: this, u: u, tx: op.Tx, silent: op.Silent, skipPermission: skipPermission})
|
||||
|
||||
return sd
|
||||
}
|
||||
|
||||
func (this *resource) GetStruct(extends ...reflect.StructField) any {
|
||||
fields := []reflect.StructField{}
|
||||
|
||||
for _, field := range this.fields {
|
||||
code := field.GetCode()
|
||||
if unicode.IsLetter(rune(code[0])) {
|
||||
fields = append(fields, field.ToStructField())
|
||||
}
|
||||
}
|
||||
|
||||
fields = lo.UniqBy(append(fields, extends...), func(v reflect.StructField) string {
|
||||
return v.Name
|
||||
})
|
||||
|
||||
t := reflect.StructOf(fields)
|
||||
|
||||
return reflect.New(t).Interface()
|
||||
}
|
||||
|
||||
func (this *resource) GetSliceStruct(extends ...reflect.StructField) any {
|
||||
t := reflect.TypeOf(this.GetStruct(extends...))
|
||||
st := reflect.SliceOf(t.Elem())
|
||||
return reflect.New(st).Interface()
|
||||
}
|
||||
|
||||
// isLocalDB 判断是否为本地文件型数据库(LastInsertId 返回最后一条而非第一条)
|
||||
func isLocalDB(dialect string) bool {
|
||||
return dialect == "sqlite3" || dialect == "duckdb"
|
||||
}
|
||||
Reference in New Issue
Block a user