package blueprint import ( "fmt" "strings" ) // MergeResult describes what was added/updated when merging incoming into base. type MergeResult struct { AddedPages []string AddedEntities []string AddedResources []string AddedFields []string // entity.field UpdatedPages []string UpdatedResources []string } // MergeInto merges incoming pages/entities/apis into base (existing published app). // Existing entities keep their fields; new fields on known entities are appended. // Same-path resources union operations (e.g. add import). // Same page id:合并 actions,不再报错(支持「只改 API/按钮、不新建页」的编辑发布)。 // 仅 route 冲突且 id 不同仍拒绝。 func MergeInto(base, incoming *Blueprint) (*MergeResult, error) { if base == nil { return nil, fmt.Errorf("base blueprint is nil") } if incoming == nil { return nil, fmt.Errorf("incoming blueprint is nil") } res := &MergeResult{} entityByName := map[string]int{} for i, e := range base.Entities { entityByName[e.Name] = i } for _, e := range incoming.Entities { if idx, ok := entityByName[e.Name]; ok { before := len(base.Entities[idx].Fields) base.Entities[idx] = mergeEntity(base.Entities[idx], e) if len(base.Entities[idx].Fields) > before { for _, f := range base.Entities[idx].Fields[before:] { res.AddedFields = append(res.AddedFields, e.Name+"."+f.Name) } } continue } base.Entities = append(base.Entities, e) entityByName[e.Name] = len(base.Entities) - 1 res.AddedEntities = append(res.AddedEntities, e.Name) } pathKey := func(p string) string { p = strings.TrimPrefix(strings.TrimSpace(p), "/") return p } resByPath := map[string]int{} for i, r := range base.Apis.Resources { resByPath[pathKey(r.Path)] = i } for _, r := range incoming.Apis.Resources { k := pathKey(r.Path) if idx, ok := resByPath[k]; ok { before := len(base.Apis.Resources[idx].Operations) base.Apis.Resources[idx] = mergeResource(base.Apis.Resources[idx], r) if len(base.Apis.Resources[idx].Operations) > before { res.UpdatedResources = append(res.UpdatedResources, k) } continue } base.Apis.Resources = append(base.Apis.Resources, r) resByPath[k] = len(base.Apis.Resources) - 1 res.AddedResources = append(res.AddedResources, k) } pageByID := map[string]int{} routeBy := map[string]string{} // route -> page id for i, p := range base.Pages { pageByID[p.ID] = i routeBy[p.Route] = p.ID } for _, p := range incoming.Pages { if idx, ok := pageByID[p.ID]; ok { before := pageActionCount(base.Pages[idx]) base.Pages[idx] = mergePage(base.Pages[idx], p) if pageActionCount(base.Pages[idx]) > before { res.UpdatedPages = append(res.UpdatedPages, p.ID) } continue } if owner, ok := routeBy[p.Route]; ok && owner != p.ID { return nil, fmt.Errorf("page route already exists: %s", p.Route) } base.Pages = append(base.Pages, p) pageByID[p.ID] = len(base.Pages) - 1 routeBy[p.Route] = p.ID res.AddedPages = append(res.AddedPages, p.ID) } if incoming.Meta.Description != "" && base.Meta.Description == "" { base.Meta.Description = incoming.Meta.Description } if incoming.Meta.UIPreset != "" && base.Meta.UIPreset == "" { base.Meta.UIPreset = incoming.Meta.UIPreset } if len(incoming.Meta.UI) > 0 && len(base.Meta.UI) == 0 { base.Meta.UI = incoming.Meta.UI } if incoming.Seed != nil && base.Seed == nil { base.Seed = incoming.Seed } if len(res.AddedPages) == 0 && len(res.AddedEntities) == 0 && len(res.AddedResources) == 0 && len(res.AddedFields) == 0 && len(res.UpdatedPages) == 0 && len(res.UpdatedResources) == 0 { return nil, fmt.Errorf("nothing new to publish: provide newly generated pages (and entities/apis if needed) for an existing app") } return res, nil } func mergeEntity(base, incoming Entity) Entity { fieldBy := map[string]struct{}{} for _, f := range base.Fields { fieldBy[f.Name] = struct{}{} } for _, f := range incoming.Fields { if _, ok := fieldBy[f.Name]; ok { continue } base.Fields = append(base.Fields, f) fieldBy[f.Name] = struct{}{} } idxBy := map[string]struct{}{} for _, ix := range base.Indexes { idxBy[ix.Name] = struct{}{} } for _, ix := range incoming.Indexes { if _, ok := idxBy[ix.Name]; ok { continue } base.Indexes = append(base.Indexes, ix) } if base.Label == "" && incoming.Label != "" { base.Label = incoming.Label } return base } func pageActionCount(p Page) int { if p.Layout == nil { return 0 } return len(p.Layout.Actions) } func mergePage(base, incoming Page) Page { if incoming.Layout == nil || len(incoming.Layout.Actions) == 0 { return base } if base.Layout == nil { base.Layout = &PageLayout{} } actSet := map[string]struct{}{} for _, a := range base.Layout.Actions { actSet[a] = struct{}{} } for _, a := range incoming.Layout.Actions { if _, ok := actSet[a]; ok { continue } base.Layout.Actions = append(base.Layout.Actions, a) actSet[a] = struct{}{} } return base } func mergeResource(base, incoming APIResource) APIResource { opSet := map[string]struct{}{} for _, op := range base.Operations { opSet[op] = struct{}{} } for _, op := range incoming.Operations { if _, ok := opSet[op]; ok { continue } base.Operations = append(base.Operations, op) opSet[op] = struct{}{} } if base.List == nil && incoming.List != nil { base.List = incoming.List } else if base.List != nil && incoming.List != nil { filt := map[string]struct{}{} for _, f := range base.List.AllowedFilters { filt[f] = struct{}{} } for _, f := range incoming.List.AllowedFilters { if _, ok := filt[f]; !ok { base.List.AllowedFilters = append(base.List.AllowedFilters, f) filt[f] = struct{}{} } } sorts := map[string]struct{}{} for _, s := range base.List.AllowedSorts { sorts[s] = struct{}{} } for _, s := range incoming.List.AllowedSorts { if _, ok := sorts[s]; !ok { base.List.AllowedSorts = append(base.List.AllowedSorts, s) sorts[s] = struct{}{} } } } return base }