chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
163
platform/internal/blueprint/merge.go
Normal file
163
platform/internal/blueprint/merge.go
Normal file
@@ -0,0 +1,163 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user