package blueprint import ( "fmt" "strings" ) // MergeResult describes what was added when merging incoming into base. type MergeResult struct { AddedPages []string AddedEntities []string AddedResources []string } // MergeInto merges incoming pages/entities/apis into base (existing published app). // Existing entities keep their fields; new fields on known entities are appended. // Pages with the same id or route are rejected; new pages are appended. 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 { base.Entities[idx] = mergeEntity(base.Entities[idx], e) 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 { base.Apis.Resources[idx] = mergeResource(base.Apis.Resources[idx], r) 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]struct{}{} routeBy := map[string]struct{}{} for _, p := range base.Pages { pageByID[p.ID] = struct{}{} routeBy[p.Route] = struct{}{} } for _, p := range incoming.Pages { if _, ok := pageByID[p.ID]; ok { return nil, fmt.Errorf("page id already exists: %s (use a new page id when adding to an existing app)", p.ID) } if _, ok := routeBy[p.Route]; ok { return nil, fmt.Errorf("page route already exists: %s", p.Route) } base.Pages = append(base.Pages, p) pageByID[p.ID] = struct{}{} routeBy[p.Route] = struct{}{} 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 { 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 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 }