diff --git a/api/openapi.yaml b/api/openapi.yaml index cb0f19c54..36389beac 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -820,6 +820,11 @@ paths: required: false in: query schema: { $ref: "#/components/schemas/Identifier" } + - name: author + description: Show only results owned by this account. + required: false + in: query + schema: { $ref: "#/components/schemas/AccountHandle" } responses: default: { $ref: "#/components/responses/InternalServerError" } "404": { $ref: "#/components/responses/NotFound" } diff --git a/app/resources/cluster_traversal/cluster_traversal.go b/app/resources/cluster_traversal/cluster_traversal.go index 0c5a7f3be..db5fcafc0 100644 --- a/app/resources/cluster_traversal/cluster_traversal.go +++ b/app/resources/cluster_traversal/cluster_traversal.go @@ -7,7 +7,19 @@ import ( ) type Repository interface { - Root(ctx context.Context) ([]*datagraph.Cluster, error) - Subtree(ctx context.Context, id datagraph.ClusterID) ([]*datagraph.Cluster, error) + Root(ctx context.Context, opts ...Filter) ([]*datagraph.Cluster, error) + Subtree(ctx context.Context, id datagraph.ClusterID, opts ...Filter) ([]*datagraph.Cluster, error) FilterSubtree(ctx context.Context, id datagraph.ClusterID, filter string) ([]*datagraph.Cluster, error) } + +type filters struct { + accountSlug *string +} + +type Filter func(*filters) + +func WithOwner(v string) Filter { + return func(f *filters) { + f.accountSlug = &v + } +} diff --git a/app/resources/cluster_traversal/db.go b/app/resources/cluster_traversal/db.go index 6f3b49a8f..b8df30bdf 100644 --- a/app/resources/cluster_traversal/db.go +++ b/app/resources/cluster_traversal/db.go @@ -12,10 +12,11 @@ import ( "github.com/jmoiron/sqlx" "github.com/rs/xid" - "github.com/Southclaws/storyden/app/resources/account" + account_repo "github.com/Southclaws/storyden/app/resources/account" "github.com/Southclaws/storyden/app/resources/datagraph" "github.com/Southclaws/storyden/app/resources/profile" "github.com/Southclaws/storyden/internal/ent" + "github.com/Southclaws/storyden/internal/ent/account" "github.com/Southclaws/storyden/internal/ent/cluster" ) @@ -28,9 +29,18 @@ func New(db *ent.Client, raw *sqlx.DB) Repository { return &database{db, raw} } -func (d *database) Root(ctx context.Context) ([]*datagraph.Cluster, error) { +func (d *database) Root(ctx context.Context, fs ...Filter) ([]*datagraph.Cluster, error) { query := d.db.Cluster.Query().Where(cluster.ParentClusterIDIsNil()).WithOwner() + f := filters{} + for _, fn := range fs { + fn(&f) + } + + if f.accountSlug != nil { + query.Where(cluster.HasOwnerWith(account.Handle(*f.accountSlug))) + } + cs, err := query.All(ctx) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) @@ -132,7 +142,7 @@ func fromRow(r subtreeRow) (*datagraph.Cluster, error) { ImageURL: opt.NewPtr(r.ClusterImageUrl), Description: r.ClusterDescription, Owner: profile.Profile{ - ID: account.AccountID(r.OwnerId), + ID: account_repo.AccountID(r.OwnerId), Handle: r.OwnerHandle, Name: r.OwnerName, Bio: opt.NewPtr(r.OwnerBio).OrZero(), @@ -142,9 +152,20 @@ func fromRow(r subtreeRow) (*datagraph.Cluster, error) { }, nil } -func (d *database) Subtree(ctx context.Context, id datagraph.ClusterID) ([]*datagraph.Cluster, error) { - filters := "" - r, err := d.raw.QueryxContext(ctx, fmt.Sprintf(ddl, filters), id.String()) +func (d *database) Subtree(ctx context.Context, id datagraph.ClusterID, fs ...Filter) ([]*datagraph.Cluster, error) { + f := filters{} + for _, fn := range fs { + fn(&f) + } + + predicates := "" + predicateN := 2 + if f.accountSlug != nil { + predicates = fmt.Sprintf("%s AND a.handle = $%d", predicates, predicateN) + // predicateN++ // Do this when more predicates are added. + } + + r, err := d.raw.QueryxContext(ctx, fmt.Sprintf(ddl, predicates), id.String()) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } diff --git a/app/transports/openapi/bindings/clusters.go b/app/transports/openapi/bindings/clusters.go index 9be843177..994582648 100644 --- a/app/transports/openapi/bindings/clusters.go +++ b/app/transports/openapi/bindings/clusters.go @@ -76,18 +76,24 @@ func (c *Clusters) ClusterList(ctx context.Context, request openapi.ClusterListR var cs []*datagraph.Cluster var err error + opts := []cluster_traversal.Filter{} + + if v := request.Params.Author; v != nil { + opts = append(opts, cluster_traversal.WithOwner(*v)) + } + if id := request.Params.ClusterId; id != nil { cid, err := xid.FromString(*id) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx), ftag.With(ftag.InvalidArgument)) } - cs, err = c.ctr.Subtree(ctx, datagraph.ClusterID(cid)) + cs, err = c.ctr.Subtree(ctx, datagraph.ClusterID(cid), opts...) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } } else { - cs, err = c.ctr.Root(ctx) + cs, err = c.ctr.Root(ctx, opts...) if err != nil { return nil, fault.Wrap(err, fctx.With(ctx)) } diff --git a/internal/openapi/generated.go b/internal/openapi/generated.go index 779da9cc6..6bd1a0979 100644 --- a/internal/openapi/generated.go +++ b/internal/openapi/generated.go @@ -1906,6 +1906,9 @@ type AssetUploadParams struct { type ClusterListParams struct { // ClusterId List this cluster and all child clusters. ClusterId *Identifier `form:"cluster_id,omitempty" json:"cluster_id,omitempty"` + + // Author Show only results owned by this account. + Author *AccountHandle `form:"author,omitempty" json:"author,omitempty"` } // IconUploadParams defines parameters for IconUpload. @@ -4179,6 +4182,22 @@ func NewClusterListRequest(server string, params *ClusterListParams) (*http.Requ } + if params.Author != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "author", runtime.ParamLocationQuery, *params.Author); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + queryURL.RawQuery = queryValues.Encode() req, err := http.NewRequest("GET", queryURL.String(), nil) @@ -10116,6 +10135,13 @@ func (w *ServerInterfaceWrapper) ClusterList(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter cluster_id: %s", err)) } + // ------------- Optional query parameter "author" ------------- + + err = runtime.BindQueryParameter("form", true, false, "author", ctx.QueryParams(), ¶ms.Author) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter author: %s", err)) + } + // Invoke the callback with all the unmarshalled arguments err = w.Handler.ClusterList(ctx, params) return err @@ -15102,38 +15128,38 @@ var swaggerSpec = []string{ "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/tests/cluster/cluster_test.go b/tests/cluster/cluster_test.go index 2cb25d97e..5d0bc9452 100644 --- a/tests/cluster/cluster_test.go +++ b/tests/cluster/cluster_test.go @@ -206,6 +206,76 @@ func TestClustersHappyPath(t *testing.T) { })) } +func TestClustersFiltering(t *testing.T) { + t.Parallel() + + integration.Test(t, nil, e2e.Setup(), fx.Invoke(func( + lc fx.Lifecycle, + ctx context.Context, + cl *openapi.ClientWithResponses, + cj *bindings.CookieJar, + ar account.Repository, + ) { + lc.Append(fx.StartHook(func() { + r := require.New(t) + a := assert.New(t) + + ctx1, acc1 := e2e.WithAccount(ctx, ar, seed.Account_001_Odin) + ctx2, acc2 := e2e.WithAccount(ctx, ar, seed.Account_002_Frigg) + + name1 := "test-cluster-owned-by-1" + slug1 := name1 + uuid.NewString() + content1 := "# Clusters\n\nOwned by Odin." + clus1, err := cl.ClusterCreateWithResponse(ctx, openapi.ClusterInitialProps{ + Name: name1, + Slug: slug1, + Description: "testing clusters api", + Content: &content1, + }, e2e.WithSession(ctx1, cj)) + r.NoError(err) + r.NotNil(clus1) + r.Equal(200, clus1.StatusCode()) + + name2 := "test-cluster-owned-by-2" + slug2 := name2 + uuid.NewString() + content2 := "# Clusters\n\nOwned by Frigg." + clus2, err := cl.ClusterCreateWithResponse(ctx, openapi.ClusterInitialProps{ + Name: name2, + Slug: slug2, + Description: "testing clusters api", + Content: &content2, + }, e2e.WithSession(ctx2, cj)) + r.NoError(err) + r.NotNil(clus1) + r.Equal(200, clus1.StatusCode()) + + clist, err := cl.ClusterListWithResponse(ctx, &openapi.ClusterListParams{ + Author: &acc1.Handle, + }) + r.NoError(err) + r.NotNil(clist) + r.Equal(200, clist.StatusCode()) + + ids := dt.Map(clist.JSON200.Clusters, func(c openapi.Cluster) string { return c.Id }) + + a.Contains(ids, clus1.JSON200.Id) + a.NotContains(ids, clus2.JSON200.Id) + + clist2, err := cl.ClusterListWithResponse(ctx, &openapi.ClusterListParams{ + Author: &acc2.Handle, + }) + r.NoError(err) + r.NotNil(clist2) + r.Equal(200, clist2.StatusCode()) + + ids2 := dt.Map(clist2.JSON200.Clusters, func(c openapi.Cluster) string { return c.Id }) + + a.NotContains(ids2, clus1.JSON200.Id) + a.Contains(ids2, clus2.JSON200.Id) + })) + })) +} + func TestClustersErrors(t *testing.T) { t.Parallel()