refactor(control-plane): resolve names server-side, drop client-side filtering · Entire

refactor(control-plane): resolve names server-side, drop client-side filtering

d7a7ef2·

toothbrush·3w ago·3 files·+111 added/-215 removed

The control plane now does O(1), case-insensitive by-name lookups for orgs, projects, and org-scoped projects (lower(name) indexes). Delegate to it and delete the CLI's "list everything then filter" code.

- regenerate coreapi client: ListOrgs gains a name param + singular org in its response; GET /orgs/{orgId}/projects gains a name param + singular project (spec hand-authored ahead of deploy; idempotent on real refresh) - resolveOrgRef/resolveProjectRef read the singular match the server returns and map 404 to a friendly "no X named" error - project list --org --name now filters server-side instead of client-side - drop pickOrg, pickProject, filterProjectsByName

Also fixes a latent bug: the name paths read out.Projects, but the server returns a name match under the singular out.Project, so an existing project resolved as "not found". Now reads the correct field.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sessions

bb17fef4a163View transcript

Changes

3

92 unmodified lines

93
94
95
96
97
98
99
96
97
98
99
100
101
102
103
104
105
105
106
107
108
109
110
111
112
113
114
115
109
116
117
118
119
120
111
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

92 unmodified lines

Args:  cobra.NoArgs,
    RunE: func(cmd *cobra.Command, _ []string) error {
        return runCoreList(cmd, projectColumns, projectRow, func(ctx context.Context, c *coreapi.Client) ([]coreapi.Project, error) {
            // --org scopes to one org's projects via the org-scoped
            // endpoint, which has no name parameter, so --name is applied
            // client-side. Without --org we use the global list, where the
            // server filters by name for us.
            // Both the global and org-scoped list endpoints filter by name
            // server-side (case-insensitive), returning the single match
            // under the response's `project` field or 404. Listing by a name
            // that doesn't exist is an empty result, not an error.
            if org != "" {
                orgID, err := resolveOrgRef(ctx, c, org)
                if err != nil {
                    return nil, err
                }
                out, err := c.ListOrgProjects(ctx, coreapi.ListOrgProjectsParams{OrgId: orgID})
                params := coreapi.ListOrgProjectsParams{OrgId: orgID};
                if name != "" {
                    params.Name = coreapi.NewOptString(name)
                }
                out, err := c.ListOrgProjects(ctx, params)
                if err != nil {
                    if name != "" && isCoreNotFound(err) {
                        return nil, nil
                    }
                    return nil, err
                }
                return filterProjectsByName(out.Projects, name), nil
                if name != "" {
                    return toProjectList(out.Project), nil
                }
                return out.Projects, nil
            }
            var params coreapi.ListProjectsParams
            params := coreapi.ListProjectsParams{}
            if name != "" {
                params.Name = coreapi.NewOptString(name)
            }
            out, err := c.ListProjects(ctx, params)
            if err != nil {
                if name != "" && isCoreNotFound(err) {
                    return nil, nil
                }
                return nil, err
            }
            if name != "" {
                return toProjectList(out.Project), nil
            }
            return out.Projects, nil
        })
    },

Mcmd/entire/cli/project.go+23/-7

1 unmodified line

2
3
4
5
6
7
8
9
10
3 unmodified lines

14
15
16
15
16
17
18
19
20
21
22
23
24
16 unmodified lines

41
42
43
44
45
46
47
48
49
50
51
52
40
53
54
55
56
57
58
45
59
60
61
62
63
64
65
49
66
67
68
69
70
71
72
73
34 unmodified lines

108
109
110
90
91
92
93
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
132
133
134
135
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
136
137
138
139
160
161
162
163
164
165
166
167
140
141
142
143
144
145
146
169
170
171
172
173
174
175
147
148

1 unmodified line

import (
    "context"
    "errors"
    "fmt"
    "net/http"
    "strings"

"github.com/entireio/cli/internal/coreapi"
3 unmodified lines

// many places (repo create --project, project create --owner, grant org/project
// <id>, …). ULIDs are unfriendly to type, so these refs also accept a human
// name: looksLikeULID decides which form was given, and the resolveXRef helpers
// turn a name into its ULID via a list lookup. A ULID is always passed straight
// through with no network call, preserving the original behavior exactly.
// turn a name into its ULID. A ULID is always passed straight through with no
// network call. A name is resolved by the control plane's O(1), case-insensitive
// by-name lookup (the server matches on lower(name) and returns the single match
// under the response's singular `org`/`project` field, or 404) — the CLI never
// lists everything and filters client-side.

// looksLikeULID reports whether s has the shape of a ULID: 26 characters drawn
// from Crockford base32 (digits plus uppercase letters, excluding I, L, O, U).
16 unmodified lines

return true
}

// isCoreNotFound reports whether err is a control-plane 404. The by-name lookups
// (ListOrgs/ListProjects/ListOrgProjects with ?name=) return 404 when nothing
// matches; callers turn that into a friendly "no X named" message.
func isCoreNotFound(err error) bool {
    var se *coreapi.ErrorModelStatusCode
    return errors.As(err, &se) && se.StatusCode == http.StatusNotFound
}

// resolveOrgRef turns an org reference (ULID or name) into its ULID. A ULID is
// returned unchanged; a name is looked up against the caller's visible orgs.
// returned unchanged; a name is resolved via the server's case-insensitive
// by-name lookup.
func resolveOrgRef(ctx context.Context, c *coreapi.Client, ref string) (string, error) {
    if looksLikeULID(ref) {
        return ref, nil
    }
    out, err := c.ListOrgs(ctx)
    out, err := c.ListOrgs(ctx, coreapi.ListOrgsParams{Name: coreapi.NewOptString(ref)})
    if err != nil {
        if isCoreNotFound(err) {
            return "", noOrgNamedErr(ref)
        }
        return "", err
    }
    return pickOrg(out.Orgs, ref)
    org, ok := out.Org.Get()
    if !ok {
        return "", noOrgNamedErr(ref)
    }
    return org.ID, nil
}

// resolveAccountRef turns an account reference into its ULID. A ULID passes
34 unmodified lines

// resolveProjectRef turns a project reference (ULID or name) into its ULID. A
// ULID is returned unchanged; a name is looked up via the server's name filter
// (the same call `entire project list --name` uses). Name matching is
// case-insensitive end to end: the server enforces lower(name) uniqueness and
// pickProject re-checks with EqualFold, so case-only differences resolve.
// ULID is returned unchanged; a name is resolved via the server's
// case-insensitive by-name lookup (the same call `entire project list --name`
// uses). Project names are globally unique, so a name maps to at most one project.
func resolveProjectRef(ctx context.Context, c *coreapi.Client, ref string) (string, error) {
    if looksLikeULID(ref) {
        return ref, nil
    }
    out, err := c.ListProjects(ctx, coreapi.ListProjectsParams{Name: coreapi.NewOptString(ref)})
    if err != nil {
        if isCoreNotFound(err) {
            return "", noProjectNamedErr(ref)
        }
        return "", err
    }
    return pickProject(out.Projects, ref)
    project, ok := out.Project.Get()
    if !ok {
        return "", noProjectNamedErr(ref)
    }
    return project.ID, nil
}

// pickOrg selects the single org named name. Org names are unique, so a name
// matches at most one org; zero matches is an error pointing at `org list`, and
// (defensively) multiple matches list the colliding ids so the user can fall
// back to a ULID. Matching is case-insensitive (EqualFold): pickOrg filters the
// full ListOrgs result client-side, so a case-only typo should still resolve.
func pickOrg(orgs []coreapi.Org, name string) (string, error) {
    var matches []coreapi.Org
    for _, o := range orgs {
        if strings.EqualFold(o.Name, name) {
            matches = append(matches, o)
        }
    }
    switch len(matches) {
    case 1:
        return matches[0].ID, nil
    case 0:
        return "", fmt.Errorf("no org named %q (run `entire org list` to see names, or pass a ULID)", name)
    default:
        ids := make([]string, len(matches))
        for i, o := range matches {
            ids[i] = o.ID
        }
        return "", fmt.Errorf("org name %q is ambiguous (%s); pass a ULID instead", name, strings.Join(ids, ", "))
    }
}
func noOrgNamedErr(name string) error {
    return fmt.Errorf("no org named %q (run `entire org list` to see names, or pass a ULID)", name)
}

// pickProject selects the single project named name. Project names are unique
// only within an owner, so a bare name can match several projects across
// different orgs/accounts; on ambiguity the candidates (id + owner) are listed
// so the user can pass the intended ULID. (We can't suggest re-scoping the
// failing command: resolveProjectRef's callers — repo create/list, grant
// project … — have no --org flag; only `entire project list` does.) Matching is
// case-insensitive (EqualFold) so the client-side re-check never drops a row the
// server returned, regardless of whether the server's name filter folds case.
func pickProject(projects []coreapi.Project, name string) (string, error) {
    var matches []coreapi.Project
    for _, p := range projects {
        if strings.EqualFold(p.Name, name) {
            matches = append(matches, p)
        }
    }
switch len(matches) {
    case 1:
        return matches[0].ID, nil
    case 0:
        return "", fmt.Errorf("no project named %q (run `entire project list` to see names, or pass a ULID)", name)
    default:
        parts := make([]string, len(matches))
        for i, p := range matches {
            parts[i] = fmt.Sprintf("%s (owner %s)", p.ID, p.OwnerId)
        }
        return "", fmt.Errorf("project name %q is ambiguous (%s); pass the intended ULID (run `entire project list --org <org>` to find it)", name, strings.Join(parts, ", "))
    }
}
func noProjectNamedErr(name string) error {
    return fmt.Errorf("no project named %q (run `entire project list` to see names, or pass a ULID)", name)
}

// filterProjectsByName narrows projects to name matches, returning all of them
// when name is empty. Used by `project list --org` to apply --name client-side,
// since the org-scoped list endpoint has no name parameter. Matching is
// case-insensitive (EqualFold), consistent with pickProject and the server's
// lower(name) uniqueness guarantee.
func filterProjectsByName(projects []coreapi.Project, name string) []coreapi.Project {
    if name == "" {
        return projects
    // toProjectList adapts a name-filtered project response — which returns the
    // single match under the response's singular `project` field — into a slice for
    // list output (empty when the field is unset).
}

func toProjectList(p coreapi.OptProject) []coreapi.Project {
    if v, ok := p.Get(); ok {
        return []coreapi.Project{v}
    }
    var out []coreapi.Project
    for _, p := range projects {
        if strings.EqualFold(p.Name, name) {
            out = append(out, p)
        }
    }
    return out
}
return nil
}

Mcmd/entire/cli/resolveref_test.go+40/-132


3 unmodified lines