feat: harden loose-offline sync for user JWT, schema, and console ops
Enable Binding-scoped agent push/pull, empty-table schema ensure, SyncPage inspect/drop-table, default module import, and agent-bound publish docs from the 宇恒联调意见. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
165
platform/internal/blueprint/ensure_ops.go
Normal file
165
platform/internal/blueprint/ensure_ops.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package blueprint
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// defaultImportExportOps Z9:业务表默认允许导入/导出。
|
||||
var defaultImportExportOps = []string{"import", "export"}
|
||||
|
||||
// defaultListActions Z9:列表页默认工具栏(含导入)。
|
||||
var defaultListActions = []string{"create", "edit", "delete", "export", "import", "refresh"}
|
||||
|
||||
// EnsureDefaultImportExport 发布/合并兜底(Z9d):
|
||||
// 可写业务 resource 缺 import/export 则补上;list 页 actions 缺 import 则补上。
|
||||
// 跳过:只读 resource(仅 list/get);meta.ui.disable_default_import == true。
|
||||
func (bp *Blueprint) EnsureDefaultImportExport() {
|
||||
if bp == nil {
|
||||
return
|
||||
}
|
||||
if disableDefaultImport(bp) {
|
||||
return
|
||||
}
|
||||
for i := range bp.Apis.Resources {
|
||||
r := &bp.Apis.Resources[i]
|
||||
if resourceReadOnly(r.Operations) {
|
||||
continue
|
||||
}
|
||||
r.Operations = ensureOpList(r.Operations, defaultImportExportOps...)
|
||||
}
|
||||
for i := range bp.Pages {
|
||||
p := &bp.Pages[i]
|
||||
if !strings.EqualFold(strings.TrimSpace(p.Type), "list") {
|
||||
continue
|
||||
}
|
||||
if p.Layout == nil {
|
||||
p.Layout = &PageLayout{}
|
||||
}
|
||||
p.Layout.Actions = ensureListActions(p.Layout.Actions)
|
||||
if p.Layout.ActionLabels == nil {
|
||||
p.Layout.ActionLabels = map[string]string{}
|
||||
}
|
||||
if _, ok := p.Layout.ActionLabels["import"]; !ok {
|
||||
p.Layout.ActionLabels["import"] = "导入 Excel"
|
||||
}
|
||||
if _, ok := p.Layout.ActionLabels["export"]; !ok {
|
||||
p.Layout.ActionLabels["export"] = "导出 Excel"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func disableDefaultImport(bp *Blueprint) bool {
|
||||
if len(bp.Meta.UI) == 0 {
|
||||
return false
|
||||
}
|
||||
var ui map[string]any
|
||||
if err := json.Unmarshal(bp.Meta.UI, &ui); err != nil {
|
||||
return false
|
||||
}
|
||||
v, ok := ui["disable_default_import"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case bool:
|
||||
return t
|
||||
case string:
|
||||
return strings.EqualFold(t, "true") || t == "1"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func resourceReadOnly(ops []string) bool {
|
||||
if len(ops) == 0 {
|
||||
return false
|
||||
}
|
||||
writable := false
|
||||
for _, op := range ops {
|
||||
switch strings.ToLower(strings.TrimSpace(op)) {
|
||||
case "create", "update", "delete", "import", "export":
|
||||
writable = true
|
||||
}
|
||||
}
|
||||
// 仅 list/get → 只读,不强加 import
|
||||
if !writable {
|
||||
onlyRead := true
|
||||
for _, op := range ops {
|
||||
switch strings.ToLower(strings.TrimSpace(op)) {
|
||||
case "list", "get", "":
|
||||
default:
|
||||
onlyRead = false
|
||||
}
|
||||
}
|
||||
return onlyRead
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ensureOpList(ops []string, add ...string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
out := make([]string, 0, len(ops)+len(add))
|
||||
for _, op := range ops {
|
||||
op = strings.ToLower(strings.TrimSpace(op))
|
||||
if op == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[op]; ok {
|
||||
continue
|
||||
}
|
||||
seen[op] = struct{}{}
|
||||
out = append(out, op)
|
||||
}
|
||||
for _, op := range add {
|
||||
op = strings.ToLower(strings.TrimSpace(op))
|
||||
if op == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[op]; ok {
|
||||
continue
|
||||
}
|
||||
seen[op] = struct{}{}
|
||||
out = append(out, op)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ensureListActions(actions []string) []string {
|
||||
if len(actions) == 0 {
|
||||
return append([]string{}, defaultListActions...)
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
out := make([]string, 0, len(actions)+2)
|
||||
for _, a := range actions {
|
||||
a = strings.ToLower(strings.TrimSpace(a))
|
||||
if a == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[a]; ok {
|
||||
continue
|
||||
}
|
||||
seen[a] = struct{}{}
|
||||
out = append(out, a)
|
||||
}
|
||||
for _, need := range []string{"import", "export"} {
|
||||
if _, ok := seen[need]; ok {
|
||||
continue
|
||||
}
|
||||
// 插在 refresh 前;若无 refresh 则追加
|
||||
inserted := false
|
||||
for i, a := range out {
|
||||
if a == "refresh" {
|
||||
out = append(out[:i], append([]string{need}, out[i:]...)...)
|
||||
seen[need] = struct{}{}
|
||||
inserted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !inserted {
|
||||
out = append(out, need)
|
||||
seen[need] = struct{}{}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user