From adf1d267f1adcc80ae64e70046f3302ae369e101 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 19:51:50 +0000 Subject: [PATCH 1/7] Initial plan From 4bb6f050ac58d3669dc1a58102a74ac58814b577 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 20:06:27 +0000 Subject: [PATCH 2/7] Add tags endpoints to nexus service with OpenAPI spec, store, controller and tests Co-authored-by: jeroenrinzema <3440116+jeroenrinzema@users.noreply.github.com> --- .../http/controllers/v1/controller.go | 2 + .../internal/http/controllers/v1/tags.go | 198 +++ .../internal/http/controllers/v1/tags_test.go | 340 ++++ services/nexus/internal/store/store.go | 2 + services/nexus/internal/store/tags.go | 132 ++ services/nexus/oapi/resources.yml | 223 +++ services/nexus/oapi/resources_gen.go | 1401 ++++++++++++++--- 7 files changed, 2120 insertions(+), 178 deletions(-) create mode 100644 services/nexus/internal/http/controllers/v1/tags.go create mode 100644 services/nexus/internal/http/controllers/v1/tags_test.go create mode 100644 services/nexus/internal/store/tags.go diff --git a/services/nexus/internal/http/controllers/v1/controller.go b/services/nexus/internal/http/controllers/v1/controller.go index df1492b3..8c8efcb0 100644 --- a/services/nexus/internal/http/controllers/v1/controller.go +++ b/services/nexus/internal/http/controllers/v1/controller.go @@ -11,6 +11,7 @@ func NewController(logger *zap.Logger, db *sqlx.DB) *Controller { TemplatesController: NewTemplatesController(logger, db), AdminsController: NewAdminsController(logger, db), UsersController: NewUsersController(logger, db), + TagsController: NewTagsController(logger, db), } } @@ -19,4 +20,5 @@ type Controller struct { *TemplatesController *AdminsController *UsersController + *TagsController } diff --git a/services/nexus/internal/http/controllers/v1/tags.go b/services/nexus/internal/http/controllers/v1/tags.go new file mode 100644 index 00000000..cf9a81ee --- /dev/null +++ b/services/nexus/internal/http/controllers/v1/tags.go @@ -0,0 +1,198 @@ +package v1 + +import ( + "database/sql" + "errors" + "net/http" + + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/lunogram/platform/pkg/http/json" + "github.com/lunogram/platform/pkg/http/problem" + "github.com/lunogram/platform/services/nexus/internal/store" + "github.com/lunogram/platform/services/nexus/oapi" + "go.uber.org/zap" +) + +func NewTagsController(logger *zap.Logger, db *sqlx.DB) *TagsController { + return &TagsController{ + logger: logger, + db: db, + store: store.NewStores(db), + } +} + +type TagsController struct { + logger *zap.Logger + db *sqlx.DB + store *store.Stores +} + +func (srv *TagsController) CreateTag(w http.ResponseWriter, r *http.Request, projectID uuid.UUID) { + ctx := r.Context() + body := oapi.CreateTagJSONRequestBody{} + err := json.Decode(r.Body, &body) + if err != nil { + oapi.WriteProblem(w, err) + return + } + + logger := srv.logger.With(zap.Stringer("project_id", projectID), zap.String("name", body.Name)) + logger.Info("creating tag") + + // Check if project exists + _, err = srv.store.ProjectsStore.GetProject(ctx, projectID) + if errors.Is(err, sql.ErrNoRows) { + logger.Error("project not found", zap.Stringer("project_id", projectID)) + oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("project not found"))) + return + } + + if err != nil { + logger.Error("failed to get project", zap.Error(err)) + oapi.WriteProblem(w, err) + return + } + + tagID, err := srv.store.TagsStore.CreateTag(ctx, projectID, body.Name) + if err != nil { + logger.Error("failed to create tag", zap.Error(err)) + oapi.WriteProblem(w, err) + return + } + + tag, err := srv.store.TagsStore.GetTag(ctx, projectID, tagID) + if err != nil { + logger.Error("failed to fetch created tag", zap.Error(err)) + oapi.WriteProblem(w, err) + return + } + + logger.Info("tag created", zap.Stringer("tag_id", tagID)) + json.Write(w, http.StatusCreated, tag.OAPI()) +} + +func (srv *TagsController) ListTags(w http.ResponseWriter, r *http.Request, projectID uuid.UUID, params oapi.ListTagsParams) { + ctx := r.Context() + logger := srv.logger.With(zap.Stringer("project_id", projectID)) + logger.Info("listing tags") + + pagination := store.Pagination{ + Limit: params.Limit.ToInt(), + Offset: params.Offset.ToInt(), + } + + search := "" + if params.Search != nil { + search = params.Search.ToString() + } + + result, total, err := srv.store.TagsStore.ListTags(ctx, projectID, pagination, search) + if err != nil { + logger.Error("failed to list tags", zap.Error(err)) + oapi.WriteProblem(w, err) + return + } + + logger.Info("listed tags", zap.Int("count", len(result))) + json.Write(w, http.StatusOK, oapi.TagListResponse{ + Total: total, + Limit: pagination.Limit, + Offset: pagination.Offset, + Results: result.OAPI(), + }) +} + +func (srv *TagsController) GetTag(w http.ResponseWriter, r *http.Request, projectID uuid.UUID, tagID uuid.UUID) { + ctx := r.Context() + logger := srv.logger.With(zap.Stringer("project_id", projectID), zap.Stringer("tag_id", tagID)) + logger.Info("getting tag") + + tag, err := srv.store.TagsStore.GetTag(ctx, projectID, tagID) + if errors.Is(err, sql.ErrNoRows) { + logger.Error("tag not found", zap.Stringer("tag_id", tagID)) + oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("tag not found"))) + return + } + + if err != nil { + logger.Error("failed to fetch tag", zap.Error(err)) + oapi.WriteProblem(w, err) + return + } + + logger.Info("tag retrieved") + json.Write(w, http.StatusOK, tag.OAPI()) +} + +func (srv *TagsController) UpdateTag(w http.ResponseWriter, r *http.Request, projectID uuid.UUID, tagID uuid.UUID) { + logger := srv.logger.With(zap.Stringer("project_id", projectID), zap.Stringer("tag_id", tagID)) + logger.Info("updating tag") + + ctx := r.Context() + body := oapi.UpdateTagJSONRequestBody{} + err := json.Decode(r.Body, &body) + if err != nil { + oapi.WriteProblem(w, err) + return + } + + _, err = srv.store.TagsStore.GetTag(ctx, projectID, tagID) + if errors.Is(err, sql.ErrNoRows) { + logger.Error("tag not found", zap.Stringer("tag_id", tagID)) + oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("tag not found"))) + return + } + + if err != nil { + logger.Error("failed to get tag", zap.Error(err)) + oapi.WriteProblem(w, err) + return + } + + err = srv.store.TagsStore.UpdateTag(ctx, projectID, tagID, body.Name) + if err != nil { + logger.Error("failed to update tag", zap.Error(err)) + oapi.WriteProblem(w, err) + return + } + + tag, err := srv.store.TagsStore.GetTag(ctx, projectID, tagID) + if err != nil { + logger.Error("failed to fetch updated tag", zap.Error(err)) + oapi.WriteProblem(w, err) + return + } + + logger.Info("tag updated") + json.Write(w, http.StatusOK, tag.OAPI()) +} + +func (srv *TagsController) DeleteTag(w http.ResponseWriter, r *http.Request, projectID uuid.UUID, tagID uuid.UUID) { + ctx := r.Context() + logger := srv.logger.With(zap.Stringer("project_id", projectID), zap.Stringer("tag_id", tagID)) + logger.Info("deleting tag") + + _, err := srv.store.TagsStore.GetTag(ctx, projectID, tagID) + if errors.Is(err, sql.ErrNoRows) { + logger.Error("tag not found", zap.Stringer("tag_id", tagID)) + oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("tag not found"))) + return + } + + if err != nil { + logger.Error("failed to get tag", zap.Error(err)) + oapi.WriteProblem(w, err) + return + } + + err = srv.store.TagsStore.DeleteTag(ctx, projectID, tagID) + if err != nil { + logger.Error("failed to delete tag", zap.Error(err)) + oapi.WriteProblem(w, err) + return + } + + logger.Info("tag deleted") + w.WriteHeader(http.StatusNoContent) +} diff --git a/services/nexus/internal/http/controllers/v1/tags_test.go b/services/nexus/internal/http/controllers/v1/tags_test.go new file mode 100644 index 00000000..72a54b45 --- /dev/null +++ b/services/nexus/internal/http/controllers/v1/tags_test.go @@ -0,0 +1,340 @@ +package v1 + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http/httptest" + "testing" + + "github.com/cloudproud/graceful" + "github.com/google/uuid" + "github.com/lunogram/platform/pkg/container" + "github.com/lunogram/platform/services/nexus/internal/config" + "github.com/lunogram/platform/services/nexus/internal/store" + "github.com/lunogram/platform/services/nexus/oapi" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestTagCreation(t *testing.T) { + logger := zaptest.NewLogger(t) + ctx := graceful.NewContext(t.Context()) + config := config.Service{ + Store: store.Config{ + URI: container.RunPostgreSQL(t), + }, + } + + err := store.Migrate(config.Store) + require.NoError(t, err) + + db, err := store.Connect(ctx, config.Store) + require.NoError(t, err) + + projects := store.NewProjectsStore(db) + projectID, err := projects.CreateProject(ctx, DefaultProject) + require.NoError(t, err) + + tags := NewTagsController(logger, db) + + type test struct { + body oapi.CreateTagJSONRequestBody + code int + } + + tests := map[string]test{ + "simple": { + body: oapi.CreateTagJSONRequestBody{ + Name: "important", + }, + code: 201, + }, + "with-spaces": { + body: oapi.CreateTagJSONRequestBody{ + Name: "very important", + }, + code: 201, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + bb, err := json.Marshal(test.body) + require.NoError(t, err) + + res := httptest.NewRecorder() + req := httptest.NewRequest("POST", "/v1/tags", bytes.NewReader(bb)) + tags.CreateTag(res, req, projectID) + + require.Equal(t, test.code, res.Code, res.Body.String()) + + if test.code == 201 { + var result oapi.Tag + err = json.Unmarshal(res.Body.Bytes(), &result) + require.NoError(t, err) + require.Equal(t, test.body.Name, result.Name) + require.Equal(t, projectID, result.ProjectId) + require.NotEqual(t, uuid.Nil, result.Id) + } + }) + } +} + +func TestListTags(t *testing.T) { + logger := zaptest.NewLogger(t) + ctx := graceful.NewContext(t.Context()) + config := config.Service{ + Store: store.Config{ + URI: container.RunPostgreSQL(t), + }, + } + + err := store.Migrate(config.Store) + require.NoError(t, err) + + db, err := store.Connect(ctx, config.Store) + require.NoError(t, err) + + projects := store.NewProjectsStore(db) + projectID, err := projects.CreateProject(ctx, DefaultProject) + require.NoError(t, err) + + tagsStore := store.NewTagsStore(db) + + // Create some test tags + tagNames := []string{"urgent", "important", "follow-up", "archived"} + for _, name := range tagNames { + _, err := tagsStore.CreateTag(ctx, projectID, name) + require.NoError(t, err) + } + + tags := NewTagsController(logger, db) + + type test struct { + params oapi.ListTagsParams + expected int + } + + tests := map[string]test{ + "list-all": { + params: oapi.ListTagsParams{ + Limit: ptr(oapi.PaginationLimit(10)), + Offset: ptr(oapi.PaginationOffset(0)), + }, + expected: 4, + }, + "with-pagination": { + params: oapi.ListTagsParams{ + Limit: ptr(oapi.PaginationLimit(2)), + Offset: ptr(oapi.PaginationOffset(0)), + }, + expected: 2, + }, + "with-search": { + params: oapi.ListTagsParams{ + Limit: ptr(oapi.PaginationLimit(10)), + Offset: ptr(oapi.PaginationOffset(0)), + Search: ptr(oapi.PaginationSearch("imp")), + }, + expected: 1, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + res := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/v1/tags", nil) + tags.ListTags(res, req, projectID, test.params) + + require.Equal(t, 200, res.Code, res.Body.String()) + + var result oapi.TagListResponse + err := json.Unmarshal(res.Body.Bytes(), &result) + require.NoError(t, err) + require.Equal(t, test.expected, len(result.Results)) + require.GreaterOrEqual(t, result.Total, test.expected) + }) + } +} + +func TestGetTag(t *testing.T) { + logger := zaptest.NewLogger(t) + ctx := graceful.NewContext(t.Context()) + config := config.Service{ + Store: store.Config{ + URI: container.RunPostgreSQL(t), + }, + } + + err := store.Migrate(config.Store) + require.NoError(t, err) + + db, err := store.Connect(ctx, config.Store) + require.NoError(t, err) + + projects := store.NewProjectsStore(db) + projectID, err := projects.CreateProject(ctx, DefaultProject) + require.NoError(t, err) + + tagsStore := store.NewTagsStore(db) + tagID, err := tagsStore.CreateTag(ctx, projectID, "test-tag") + require.NoError(t, err) + + tags := NewTagsController(logger, db) + + type test struct { + tagID uuid.UUID + code int + } + + tests := map[string]test{ + "existing-tag": { + tagID: tagID, + code: 200, + }, + "non-existing-tag": { + tagID: uuid.New(), + code: 404, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + res := httptest.NewRecorder() + req := httptest.NewRequest("GET", fmt.Sprintf("/v1/tags/%s", test.tagID), nil) + tags.GetTag(res, req, projectID, test.tagID) + + require.Equal(t, test.code, res.Code, res.Body.String()) + + if test.code == 200 { + var result oapi.Tag + err := json.Unmarshal(res.Body.Bytes(), &result) + require.NoError(t, err) + require.Equal(t, test.tagID, result.Id) + require.Equal(t, "test-tag", result.Name) + } + }) + } +} + +func TestUpdateTag(t *testing.T) { + logger := zaptest.NewLogger(t) + ctx := graceful.NewContext(t.Context()) + config := config.Service{ + Store: store.Config{ + URI: container.RunPostgreSQL(t), + }, + } + + err := store.Migrate(config.Store) + require.NoError(t, err) + + db, err := store.Connect(ctx, config.Store) + require.NoError(t, err) + + projects := store.NewProjectsStore(db) + projectID, err := projects.CreateProject(ctx, DefaultProject) + require.NoError(t, err) + + tagsStore := store.NewTagsStore(db) + tagID, err := tagsStore.CreateTag(ctx, projectID, "old-name") + require.NoError(t, err) + + tags := NewTagsController(logger, db) + + type test struct { + tagID uuid.UUID + body oapi.UpdateTagJSONRequestBody + code int + } + + tests := map[string]test{ + "successful-update": { + tagID: tagID, + body: oapi.UpdateTagJSONRequestBody{ + Name: "new-name", + }, + code: 200, + }, + "non-existing-tag": { + tagID: uuid.New(), + body: oapi.UpdateTagJSONRequestBody{ + Name: "another-name", + }, + code: 404, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + bb, err := json.Marshal(test.body) + require.NoError(t, err) + + res := httptest.NewRecorder() + req := httptest.NewRequest("PATCH", fmt.Sprintf("/v1/tags/%s", test.tagID), bytes.NewReader(bb)) + tags.UpdateTag(res, req, projectID, test.tagID) + + require.Equal(t, test.code, res.Code, res.Body.String()) + + if test.code == 200 { + var result oapi.Tag + err := json.Unmarshal(res.Body.Bytes(), &result) + require.NoError(t, err) + require.Equal(t, test.body.Name, result.Name) + } + }) + } +} + +func TestDeleteTag(t *testing.T) { + logger := zaptest.NewLogger(t) + ctx := graceful.NewContext(t.Context()) + config := config.Service{ + Store: store.Config{ + URI: container.RunPostgreSQL(t), + }, + } + + err := store.Migrate(config.Store) + require.NoError(t, err) + + db, err := store.Connect(ctx, config.Store) + require.NoError(t, err) + + projects := store.NewProjectsStore(db) + projectID, err := projects.CreateProject(ctx, DefaultProject) + require.NoError(t, err) + + tagsStore := store.NewTagsStore(db) + tagID, err := tagsStore.CreateTag(ctx, projectID, "to-delete") + require.NoError(t, err) + + tags := NewTagsController(logger, db) + + type test struct { + tagID uuid.UUID + code int + } + + tests := map[string]test{ + "successful-delete": { + tagID: tagID, + code: 204, + }, + "non-existing-tag": { + tagID: uuid.New(), + code: 404, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + res := httptest.NewRecorder() + req := httptest.NewRequest("DELETE", fmt.Sprintf("/v1/tags/%s", test.tagID), nil) + tags.DeleteTag(res, req, projectID, test.tagID) + + require.Equal(t, test.code, res.Code, res.Body.String()) + }) + } +} diff --git a/services/nexus/internal/store/store.go b/services/nexus/internal/store/store.go index b5d8b106..483d1df0 100644 --- a/services/nexus/internal/store/store.go +++ b/services/nexus/internal/store/store.go @@ -53,6 +53,7 @@ func NewStores(db DB) *Stores { JourneysStore: NewJourneysStore(db), OrganizationsStore: NewOrganizationsStore(db), DevicesStore: NewDevicesStore(db), + TagsStore: NewTagsStore(db), } } @@ -67,6 +68,7 @@ type Stores struct { *JourneysStore *OrganizationsStore *DevicesStore + *TagsStore } type Pagination struct { diff --git a/services/nexus/internal/store/tags.go b/services/nexus/internal/store/tags.go new file mode 100644 index 00000000..d6b6bb74 --- /dev/null +++ b/services/nexus/internal/store/tags.go @@ -0,0 +1,132 @@ +package store + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/lunogram/platform/services/nexus/oapi" +) + +type Tags []Tag + +func (tags Tags) OAPI() []oapi.Tag { + result := make([]oapi.Tag, len(tags)) + for index, tag := range tags { + result[index] = tag.OAPI() + } + return result +} + +type Tag struct { + ID uuid.UUID `db:"id"` + ProjectID uuid.UUID `db:"project_id"` + Name string `db:"name"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + DeletedAt *time.Time `db:"deleted_at"` +} + +func (tag Tag) OAPI() oapi.Tag { + return oapi.Tag{ + Id: tag.ID, + ProjectId: tag.ProjectID, + Name: tag.Name, + CreatedAt: tag.CreatedAt, + UpdatedAt: tag.UpdatedAt, + } +} + +func NewTagsStore(db DB) *TagsStore { + return &TagsStore{ + db: db, + } +} + +type TagsStore struct { + db DB +} + +func (s *TagsStore) CreateTag(ctx context.Context, projectID uuid.UUID, name string) (uuid.UUID, error) { + stmt := ` + INSERT INTO tags (project_id, name) + VALUES ($1, $2) + RETURNING id` + + var id uuid.UUID + err := s.db.GetContext(ctx, &id, stmt, projectID, name) + if err != nil { + return uuid.Nil, err + } + + return id, nil +} + +func (s *TagsStore) ListTags(ctx context.Context, projectID uuid.UUID, pagination Pagination, search string) (Tags, int, error) { + query := ` + SELECT id, project_id, name, created_at, updated_at, + COUNT(*) OVER () AS total_count + FROM tags + WHERE project_id = $1 + AND ($2 = '' OR name ILIKE '%' || $2 || '%') + ORDER BY name ASC + LIMIT $3 OFFSET $4` + + var results []struct { + Tag + TotalCount int `db:"total_count"` + } + err := s.db.SelectContext(ctx, &results, query, projectID, search, pagination.Limit, pagination.Offset) + if err != nil { + return nil, 0, err + } + + tags := make(Tags, len(results)) + total := 0 + + for i, r := range results { + tags[i] = r.Tag + if i == 0 { + total = r.TotalCount + } + } + + return tags, total, nil +} + +func (s *TagsStore) GetTag(ctx context.Context, projectID, tagID uuid.UUID) (*Tag, error) { + query := ` + SELECT id, project_id, name, created_at, updated_at + FROM tags + WHERE project_id = $1 + AND id = $2` + + var tag Tag + err := s.db.GetContext(ctx, &tag, query, projectID, tagID) + if err != nil { + return nil, err + } + + return &tag, nil +} + +func (s *TagsStore) UpdateTag(ctx context.Context, projectID, tagID uuid.UUID, name string) error { + stmt := ` + UPDATE tags + SET name = $1 + WHERE project_id = $2 + AND id = $3` + + _, err := s.db.ExecContext(ctx, stmt, name, projectID, tagID) + return err +} + +func (s *TagsStore) DeleteTag(ctx context.Context, projectID, tagID uuid.UUID) error { + stmt := ` + DELETE FROM tags + WHERE project_id = $1 + AND id = $2` + + _, err := s.db.ExecContext(ctx, stmt, projectID, tagID) + return err +} diff --git a/services/nexus/oapi/resources.yml b/services/nexus/oapi/resources.yml index 913f307c..b046ccf3 100644 --- a/services/nexus/oapi/resources.yml +++ b/services/nexus/oapi/resources.yml @@ -853,6 +853,166 @@ paths: default: $ref: '#/components/responses/Error' + /api/admin/projects/{projectID}/tags: + get: + summary: List tags + description: Retrieves a list of tags with optional search filtering + operationId: listTags + tags: + - Tags + security: + - HttpBearerAuth: [] + parameters: + - name: projectID + in: path + required: true + schema: + type: string + format: uuid + description: The project ID + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - $ref: '#/components/parameters/Search' + responses: + '200': + $ref: '#/components/responses/TagListResponse' + default: + $ref: '#/components/responses/Error' + + post: + summary: Create tag + description: Creates a new tag + operationId: createTag + tags: + - Tags + security: + - HttpBearerAuth: [] + parameters: + - name: projectID + in: path + required: true + schema: + type: string + format: uuid + description: The project ID + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTag' + responses: + '201': + description: Tag created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Tag' + default: + $ref: '#/components/responses/Error' + + /api/admin/projects/{projectID}/tags/{tagID}: + get: + summary: Get tag by ID + description: Retrieves a specific tag + operationId: getTag + tags: + - Tags + security: + - HttpBearerAuth: [] + parameters: + - name: projectID + in: path + required: true + schema: + type: string + format: uuid + description: The project ID + - name: tagID + in: path + required: true + schema: + type: string + format: uuid + description: The tag ID + responses: + '200': + description: Tag retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Tag' + default: + $ref: '#/components/responses/Error' + + patch: + summary: Update tag + description: Updates a specific tag + operationId: updateTag + tags: + - Tags + security: + - HttpBearerAuth: [] + parameters: + - name: projectID + in: path + required: true + schema: + type: string + format: uuid + description: The project ID + - name: tagID + in: path + required: true + schema: + type: string + format: uuid + description: The tag ID + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateTag' + responses: + '200': + description: Tag updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Tag' + default: + $ref: '#/components/responses/Error' + + delete: + summary: Delete tag + description: Deletes a specific tag + operationId: deleteTag + tags: + - Tags + security: + - HttpBearerAuth: [] + parameters: + - name: projectID + in: path + required: true + schema: + type: string + format: uuid + description: The project ID + - name: tagID + in: path + required: true + schema: + type: string + format: uuid + description: The tag ID + responses: + '204': + description: Tag deleted successfully + default: + $ref: '#/components/responses/Error' + components: parameters: Limit: @@ -911,6 +1071,22 @@ components: items: $ref: '#/components/schemas/Campaign' + TagListResponse: + description: Tags retrieved successfully + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + required: + - results + properties: + results: + type: array + items: + $ref: '#/components/schemas/Tag' + schemas: Problem: type: object @@ -1749,6 +1925,53 @@ components: items: $ref: '#/components/schemas/UserJourneyEntrance' + Tag: + type: object + required: + - id + - project_id + - name + - created_at + - updated_at + properties: + id: + type: string + format: uuid + example: "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" + project_id: + type: string + format: uuid + example: "4c9d3163-7b64-4f9e-9068-d2e4b96be56b" + name: + type: string + example: "important" + created_at: + type: string + format: date-time + example: "2025-11-19T14:18:42.960Z" + updated_at: + type: string + format: date-time + example: "2025-11-23T17:20:00.021Z" + + CreateTag: + type: object + required: + - name + properties: + name: + type: string + example: "important" + + UpdateTag: + type: object + required: + - name + properties: + name: + type: string + example: "updated-tag-name" + securitySchemes: HttpBearerAuth: type: http diff --git a/services/nexus/oapi/resources_gen.go b/services/nexus/oapi/resources_gen.go index 60b21ab2..eaa959d6 100644 --- a/services/nexus/oapi/resources_gen.go +++ b/services/nexus/oapi/resources_gen.go @@ -192,6 +192,11 @@ type CreateCampaign struct { // CreateCampaignChannel defines model for CreateCampaign.Channel. type CreateCampaignChannel string +// CreateTag defines model for CreateTag. +type CreateTag struct { + Name string `json:"name"` +} + // CreateTemplate defines model for CreateTemplate. type CreateTemplate struct { // Data Template-specific data based on type. Structure varies by template type. @@ -328,6 +333,15 @@ type SmsTemplateData struct { Body *string `json:"body,omitempty"` } +// Tag defines model for Tag. +type Tag struct { + CreatedAt time.Time `json:"created_at"` + Id openapi_types.UUID `json:"id"` + Name string `json:"name"` + ProjectId openapi_types.UUID `json:"project_id"` + UpdatedAt time.Time `json:"updated_at"` +} + // Template defines model for Template. type Template struct { CampaignId openapi_types.UUID `json:"campaign_id"` @@ -360,6 +374,11 @@ type UpdateCampaign struct { ProviderId *openapi_types.UUID `json:"provider_id,omitempty"` } +// UpdateTag defines model for UpdateTag. +type UpdateTag struct { + Name string `json:"name"` +} + // UpdateTemplate defines model for UpdateTemplate. type UpdateTemplate struct { // Data Template-specific data based on type. Structure varies by template type. @@ -515,6 +534,19 @@ type CampaignListResponse struct { // Error defines model for Error. type Error = Problem +// TagListResponse defines model for TagListResponse. +type TagListResponse struct { + // Limit Maximum number of items returned + Limit int `json:"limit"` + + // Offset Number of items skipped + Offset int `json:"offset"` + Results []Tag `json:"results"` + + // Total Total number of items matching the filters + Total int `json:"total"` +} + // ListAdminsParams defines parameters for ListAdmins. type ListAdminsParams struct { // Limit Maximum number of items to return @@ -545,6 +577,18 @@ type GetCampaignUsersParams struct { Offset *Offset `form:"offset,omitempty" json:"offset,omitempty"` } +// ListTagsParams defines parameters for ListTags. +type ListTagsParams struct { + // Limit Maximum number of items to return + Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` + + // Offset Number of items to skip + Offset *Offset `form:"offset,omitempty" json:"offset,omitempty"` + + // Search Search query string + Search *Search `form:"search,omitempty" json:"search,omitempty"` +} + // ListUsersParams defines parameters for ListUsers. type ListUsersParams struct { // Limit Maximum number of items to return @@ -602,6 +646,12 @@ type CreateTemplateJSONRequestBody = CreateTemplate // UpdateTemplateJSONRequestBody defines body for UpdateTemplate for application/json ContentType. type UpdateTemplateJSONRequestBody = UpdateTemplate +// CreateTagJSONRequestBody defines body for CreateTag for application/json ContentType. +type CreateTagJSONRequestBody = CreateTag + +// UpdateTagJSONRequestBody defines body for UpdateTag for application/json ContentType. +type UpdateTagJSONRequestBody = UpdateTag + // IdentifyUserJSONRequestBody defines body for IdentifyUser for application/json ContentType. type IdentifyUserJSONRequestBody = IdentifyUser @@ -924,6 +974,25 @@ type ClientInterface interface { // GetCampaignUsers request GetCampaignUsers(ctx context.Context, projectID openapi_types.UUID, campaignID openapi_types.UUID, params *GetCampaignUsersParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListTags request + ListTags(ctx context.Context, projectID openapi_types.UUID, params *ListTagsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // CreateTagWithBody request with any body + CreateTagWithBody(ctx context.Context, projectID openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreateTag(ctx context.Context, projectID openapi_types.UUID, body CreateTagJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteTag request + DeleteTag(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetTag request + GetTag(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // UpdateTagWithBody request with any body + UpdateTagWithBody(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + UpdateTag(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, body UpdateTagJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListUsers request ListUsers(ctx context.Context, projectID openapi_types.UUID, params *ListUsersParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1222,6 +1291,90 @@ func (c *Client) GetCampaignUsers(ctx context.Context, projectID openapi_types.U return c.Client.Do(req) } +func (c *Client) ListTags(ctx context.Context, projectID openapi_types.UUID, params *ListTagsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListTagsRequest(c.Server, projectID, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateTagWithBody(ctx context.Context, projectID openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateTagRequestWithBody(c.Server, projectID, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateTag(ctx context.Context, projectID openapi_types.UUID, body CreateTagJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateTagRequest(c.Server, projectID, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteTag(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteTagRequest(c.Server, projectID, tagID) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetTag(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetTagRequest(c.Server, projectID, tagID) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UpdateTagWithBody(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateTagRequestWithBody(c.Server, projectID, tagID, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UpdateTag(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, body UpdateTagJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateTagRequest(c.Server, projectID, tagID, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) ListUsers(ctx context.Context, projectID openapi_types.UUID, params *ListUsersParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewListUsersRequest(c.Server, projectID, params) if err != nil { @@ -2174,8 +2327,8 @@ func NewGetCampaignUsersRequest(server string, projectID openapi_types.UUID, cam return req, nil } -// NewListUsersRequest generates requests for ListUsers -func NewListUsersRequest(server string, projectID openapi_types.UUID, params *ListUsersParams) (*http.Request, error) { +// NewListTagsRequest generates requests for ListTags +func NewListTagsRequest(server string, projectID openapi_types.UUID, params *ListTagsParams) (*http.Request, error) { var err error var pathParam0 string @@ -2190,7 +2343,7 @@ func NewListUsersRequest(server string, projectID openapi_types.UUID, params *Li return nil, err } - operationPath := fmt.Sprintf("/api/admin/projects/%s/users", pathParam0) + operationPath := fmt.Sprintf("/api/admin/projects/%s/tags", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -2262,19 +2415,19 @@ func NewListUsersRequest(server string, projectID openapi_types.UUID, params *Li return req, nil } -// NewIdentifyUserRequest calls the generic IdentifyUser builder with application/json body -func NewIdentifyUserRequest(server string, projectID openapi_types.UUID, body IdentifyUserJSONRequestBody) (*http.Request, error) { +// NewCreateTagRequest calls the generic CreateTag builder with application/json body +func NewCreateTagRequest(server string, projectID openapi_types.UUID, body CreateTagJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader buf, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(buf) - return NewIdentifyUserRequestWithBody(server, projectID, "application/json", bodyReader) + return NewCreateTagRequestWithBody(server, projectID, "application/json", bodyReader) } -// NewIdentifyUserRequestWithBody generates requests for IdentifyUser with any type of body -func NewIdentifyUserRequestWithBody(server string, projectID openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { +// NewCreateTagRequestWithBody generates requests for CreateTag with any type of body +func NewCreateTagRequestWithBody(server string, projectID openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { var err error var pathParam0 string @@ -2289,7 +2442,7 @@ func NewIdentifyUserRequestWithBody(server string, projectID openapi_types.UUID, return nil, err } - operationPath := fmt.Sprintf("/api/admin/projects/%s/users", pathParam0) + operationPath := fmt.Sprintf("/api/admin/projects/%s/tags", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -2309,8 +2462,8 @@ func NewIdentifyUserRequestWithBody(server string, projectID openapi_types.UUID, return req, nil } -// NewDeleteUserRequest generates requests for DeleteUser -func NewDeleteUserRequest(server string, projectID openapi_types.UUID, userID openapi_types.UUID) (*http.Request, error) { +// NewDeleteTagRequest generates requests for DeleteTag +func NewDeleteTagRequest(server string, projectID openapi_types.UUID, tagID openapi_types.UUID) (*http.Request, error) { var err error var pathParam0 string @@ -2322,7 +2475,7 @@ func NewDeleteUserRequest(server string, projectID openapi_types.UUID, userID op var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "userID", runtime.ParamLocationPath, userID) + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "tagID", runtime.ParamLocationPath, tagID) if err != nil { return nil, err } @@ -2332,7 +2485,7 @@ func NewDeleteUserRequest(server string, projectID openapi_types.UUID, userID op return nil, err } - operationPath := fmt.Sprintf("/api/admin/projects/%s/users/%s", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/api/admin/projects/%s/tags/%s", pathParam0, pathParam1) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -2350,8 +2503,8 @@ func NewDeleteUserRequest(server string, projectID openapi_types.UUID, userID op return req, nil } -// NewGetUserRequest generates requests for GetUser -func NewGetUserRequest(server string, projectID openapi_types.UUID, userID openapi_types.UUID) (*http.Request, error) { +// NewGetTagRequest generates requests for GetTag +func NewGetTagRequest(server string, projectID openapi_types.UUID, tagID openapi_types.UUID) (*http.Request, error) { var err error var pathParam0 string @@ -2363,7 +2516,7 @@ func NewGetUserRequest(server string, projectID openapi_types.UUID, userID opena var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "userID", runtime.ParamLocationPath, userID) + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "tagID", runtime.ParamLocationPath, tagID) if err != nil { return nil, err } @@ -2373,7 +2526,7 @@ func NewGetUserRequest(server string, projectID openapi_types.UUID, userID opena return nil, err } - operationPath := fmt.Sprintf("/api/admin/projects/%s/users/%s", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/api/admin/projects/%s/tags/%s", pathParam0, pathParam1) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -2391,19 +2544,19 @@ func NewGetUserRequest(server string, projectID openapi_types.UUID, userID opena return req, nil } -// NewUpdateUserRequest calls the generic UpdateUser builder with application/json body -func NewUpdateUserRequest(server string, projectID openapi_types.UUID, userID openapi_types.UUID, body UpdateUserJSONRequestBody) (*http.Request, error) { +// NewUpdateTagRequest calls the generic UpdateTag builder with application/json body +func NewUpdateTagRequest(server string, projectID openapi_types.UUID, tagID openapi_types.UUID, body UpdateTagJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader buf, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(buf) - return NewUpdateUserRequestWithBody(server, projectID, userID, "application/json", bodyReader) + return NewUpdateTagRequestWithBody(server, projectID, tagID, "application/json", bodyReader) } -// NewUpdateUserRequestWithBody generates requests for UpdateUser with any type of body -func NewUpdateUserRequestWithBody(server string, projectID openapi_types.UUID, userID openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { +// NewUpdateTagRequestWithBody generates requests for UpdateTag with any type of body +func NewUpdateTagRequestWithBody(server string, projectID openapi_types.UUID, tagID openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { var err error var pathParam0 string @@ -2415,7 +2568,7 @@ func NewUpdateUserRequestWithBody(server string, projectID openapi_types.UUID, u var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "userID", runtime.ParamLocationPath, userID) + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "tagID", runtime.ParamLocationPath, tagID) if err != nil { return nil, err } @@ -2425,7 +2578,7 @@ func NewUpdateUserRequestWithBody(server string, projectID openapi_types.UUID, u return nil, err } - operationPath := fmt.Sprintf("/api/admin/projects/%s/users/%s", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/api/admin/projects/%s/tags/%s", pathParam0, pathParam1) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -2445,8 +2598,8 @@ func NewUpdateUserRequestWithBody(server string, projectID openapi_types.UUID, u return req, nil } -// NewGetUserEventsRequest generates requests for GetUserEvents -func NewGetUserEventsRequest(server string, projectID openapi_types.UUID, userID openapi_types.UUID, params *GetUserEventsParams) (*http.Request, error) { +// NewListUsersRequest generates requests for ListUsers +func NewListUsersRequest(server string, projectID openapi_types.UUID, params *ListUsersParams) (*http.Request, error) { var err error var pathParam0 string @@ -2456,19 +2609,12 @@ func NewGetUserEventsRequest(server string, projectID openapi_types.UUID, userID return nil, err } - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "userID", runtime.ParamLocationPath, userID) - if err != nil { - return nil, err - } - serverURL, err := url.Parse(server) if err != nil { return nil, err } - operationPath := fmt.Sprintf("/api/admin/projects/%s/users/%s/events", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/api/admin/projects/%s/users", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -2513,6 +2659,22 @@ func NewGetUserEventsRequest(server string, projectID openapi_types.UUID, userID } + if params.Search != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "search", runtime.ParamLocationQuery, *params.Search); 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() } @@ -2524,20 +2686,24 @@ func NewGetUserEventsRequest(server string, projectID openapi_types.UUID, userID return req, nil } -// NewGetUserJourneysRequest generates requests for GetUserJourneys -func NewGetUserJourneysRequest(server string, projectID openapi_types.UUID, userID openapi_types.UUID, params *GetUserJourneysParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "projectID", runtime.ParamLocationPath, projectID) +// NewIdentifyUserRequest calls the generic IdentifyUser builder with application/json body +func NewIdentifyUserRequest(server string, projectID openapi_types.UUID, body IdentifyUserJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) if err != nil { return nil, err } + bodyReader = bytes.NewReader(buf) + return NewIdentifyUserRequestWithBody(server, projectID, "application/json", bodyReader) +} - var pathParam1 string +// NewIdentifyUserRequestWithBody generates requests for IdentifyUser with any type of body +func NewIdentifyUserRequestWithBody(server string, projectID openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { + var err error - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "userID", runtime.ParamLocationPath, userID) + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "projectID", runtime.ParamLocationPath, projectID) if err != nil { return nil, err } @@ -2547,7 +2713,7 @@ func NewGetUserJourneysRequest(server string, projectID openapi_types.UUID, user return nil, err } - operationPath := fmt.Sprintf("/api/admin/projects/%s/users/%s/journeys", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/api/admin/projects/%s/users", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -2557,54 +2723,18 @@ func NewGetUserJourneysRequest(server string, projectID openapi_types.UUID, user return nil, err } - if params != nil { - queryValues := queryURL.Query() - - if params.Limit != nil { - - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); 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) - } - } - } - - } - - if params.Offset != nil { - - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "offset", runtime.ParamLocationQuery, *params.Offset); 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) + req, err := http.NewRequest("POST", queryURL.String(), body) if err != nil { return nil, err } + req.Header.Add("Content-Type", contentType) + return req, nil } -// NewGetUserSubscriptionsRequest generates requests for GetUserSubscriptions -func NewGetUserSubscriptionsRequest(server string, projectID openapi_types.UUID, userID openapi_types.UUID, params *GetUserSubscriptionsParams) (*http.Request, error) { +// NewDeleteUserRequest generates requests for DeleteUser +func NewDeleteUserRequest(server string, projectID openapi_types.UUID, userID openapi_types.UUID) (*http.Request, error) { var err error var pathParam0 string @@ -2626,7 +2756,7 @@ func NewGetUserSubscriptionsRequest(server string, projectID openapi_types.UUID, return nil, err } - operationPath := fmt.Sprintf("/api/admin/projects/%s/users/%s/subscriptions", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/api/admin/projects/%s/users/%s", pathParam0, pathParam1) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -2636,24 +2766,318 @@ func NewGetUserSubscriptionsRequest(server string, projectID openapi_types.UUID, return nil, err } - if params != nil { - queryValues := queryURL.Query() - - if params.Limit != nil { + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); 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) - } - } - } + return req, nil +} - } +// NewGetUserRequest generates requests for GetUser +func NewGetUserRequest(server string, projectID openapi_types.UUID, userID openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "projectID", runtime.ParamLocationPath, projectID) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "userID", runtime.ParamLocationPath, userID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/admin/projects/%s/users/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewUpdateUserRequest calls the generic UpdateUser builder with application/json body +func NewUpdateUserRequest(server string, projectID openapi_types.UUID, userID openapi_types.UUID, body UpdateUserJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewUpdateUserRequestWithBody(server, projectID, userID, "application/json", bodyReader) +} + +// NewUpdateUserRequestWithBody generates requests for UpdateUser with any type of body +func NewUpdateUserRequestWithBody(server string, projectID openapi_types.UUID, userID openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "projectID", runtime.ParamLocationPath, projectID) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "userID", runtime.ParamLocationPath, userID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/admin/projects/%s/users/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PATCH", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewGetUserEventsRequest generates requests for GetUserEvents +func NewGetUserEventsRequest(server string, projectID openapi_types.UUID, userID openapi_types.UUID, params *GetUserEventsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "projectID", runtime.ParamLocationPath, projectID) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "userID", runtime.ParamLocationPath, userID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/admin/projects/%s/users/%s/events", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); 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) + } + } + } + + } + + if params.Offset != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "offset", runtime.ParamLocationQuery, *params.Offset); 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) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetUserJourneysRequest generates requests for GetUserJourneys +func NewGetUserJourneysRequest(server string, projectID openapi_types.UUID, userID openapi_types.UUID, params *GetUserJourneysParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "projectID", runtime.ParamLocationPath, projectID) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "userID", runtime.ParamLocationPath, userID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/admin/projects/%s/users/%s/journeys", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); 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) + } + } + } + + } + + if params.Offset != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "offset", runtime.ParamLocationQuery, *params.Offset); 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) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetUserSubscriptionsRequest generates requests for GetUserSubscriptions +func NewGetUserSubscriptionsRequest(server string, projectID openapi_types.UUID, userID openapi_types.UUID, params *GetUserSubscriptionsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "projectID", runtime.ParamLocationPath, projectID) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "userID", runtime.ParamLocationPath, userID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/admin/projects/%s/users/%s/subscriptions", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); 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) + } + } + } + + } if params.Offset != nil { @@ -2839,6 +3263,25 @@ type ClientWithResponsesInterface interface { // GetCampaignUsersWithResponse request GetCampaignUsersWithResponse(ctx context.Context, projectID openapi_types.UUID, campaignID openapi_types.UUID, params *GetCampaignUsersParams, reqEditors ...RequestEditorFn) (*GetCampaignUsersResponse, error) + // ListTagsWithResponse request + ListTagsWithResponse(ctx context.Context, projectID openapi_types.UUID, params *ListTagsParams, reqEditors ...RequestEditorFn) (*ListTagsResponse, error) + + // CreateTagWithBodyWithResponse request with any body + CreateTagWithBodyWithResponse(ctx context.Context, projectID openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateTagResponse, error) + + CreateTagWithResponse(ctx context.Context, projectID openapi_types.UUID, body CreateTagJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateTagResponse, error) + + // DeleteTagWithResponse request + DeleteTagWithResponse(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, reqEditors ...RequestEditorFn) (*DeleteTagResponse, error) + + // GetTagWithResponse request + GetTagWithResponse(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetTagResponse, error) + + // UpdateTagWithBodyWithResponse request with any body + UpdateTagWithBodyWithResponse(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateTagResponse, error) + + UpdateTagWithResponse(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, body UpdateTagJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateTagResponse, error) + // ListUsersWithResponse request ListUsersWithResponse(ctx context.Context, projectID openapi_types.UUID, params *ListUsersParams, reqEditors ...RequestEditorFn) (*ListUsersResponse, error) @@ -3056,17 +3499,133 @@ func (r CreateCampaignResponse) StatusCode() int { return 0 } -type GetCampaignResponse struct { +type GetCampaignResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + Data Campaign `json:"data"` + } + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r GetCampaignResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetCampaignResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type UpdateCampaignResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Campaign + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r UpdateCampaignResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UpdateCampaignResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DuplicateCampaignResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *Campaign + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r DuplicateCampaignResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DuplicateCampaignResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type CreateTemplateResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *Template + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r CreateTemplateResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateTemplateResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteTemplateResponse struct { + Body []byte + HTTPResponse *http.Response + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r DeleteTemplateResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteTemplateResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetTemplateResponse struct { Body []byte HTTPResponse *http.Response JSON200 *struct { - Data Campaign `json:"data"` + Data Template `json:"data"` } JSONDefault *Error } // Status returns HTTPResponse.Status -func (r GetCampaignResponse) Status() string { +func (r GetTemplateResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -3074,22 +3633,22 @@ func (r GetCampaignResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r GetCampaignResponse) StatusCode() int { +func (r GetTemplateResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type UpdateCampaignResponse struct { +type UpdateTemplateResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *Campaign + JSON200 *Template JSONDefault *Error } // Status returns HTTPResponse.Status -func (r UpdateCampaignResponse) Status() string { +func (r UpdateTemplateResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -3097,22 +3656,27 @@ func (r UpdateCampaignResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r UpdateCampaignResponse) StatusCode() int { +func (r UpdateTemplateResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type DuplicateCampaignResponse struct { +type GetCampaignUsersResponse struct { Body []byte HTTPResponse *http.Response - JSON201 *Campaign - JSONDefault *Error + JSON200 *struct { + Data []CampaignUser `json:"data"` + Limit int `json:"limit"` + Offset int `json:"offset"` + Total int `json:"total"` + } + JSONDefault *Error } // Status returns HTTPResponse.Status -func (r DuplicateCampaignResponse) Status() string { +func (r GetCampaignUsersResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -3120,22 +3684,22 @@ func (r DuplicateCampaignResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r DuplicateCampaignResponse) StatusCode() int { +func (r GetCampaignUsersResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type CreateTemplateResponse struct { +type ListTagsResponse struct { Body []byte HTTPResponse *http.Response - JSON201 *Template + JSON200 *TagListResponse JSONDefault *Error } // Status returns HTTPResponse.Status -func (r CreateTemplateResponse) Status() string { +func (r ListTagsResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -3143,21 +3707,22 @@ func (r CreateTemplateResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r CreateTemplateResponse) StatusCode() int { +func (r ListTagsResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type DeleteTemplateResponse struct { +type CreateTagResponse struct { Body []byte HTTPResponse *http.Response + JSON201 *Tag JSONDefault *Error } // Status returns HTTPResponse.Status -func (r DeleteTemplateResponse) Status() string { +func (r CreateTagResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -3165,24 +3730,21 @@ func (r DeleteTemplateResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r DeleteTemplateResponse) StatusCode() int { +func (r CreateTagResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type GetTemplateResponse struct { +type DeleteTagResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *struct { - Data Template `json:"data"` - } - JSONDefault *Error + JSONDefault *Error } // Status returns HTTPResponse.Status -func (r GetTemplateResponse) Status() string { +func (r DeleteTagResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -3190,22 +3752,22 @@ func (r GetTemplateResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r GetTemplateResponse) StatusCode() int { +func (r DeleteTagResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type UpdateTemplateResponse struct { +type GetTagResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *Template + JSON200 *Tag JSONDefault *Error } // Status returns HTTPResponse.Status -func (r UpdateTemplateResponse) Status() string { +func (r GetTagResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -3213,27 +3775,22 @@ func (r UpdateTemplateResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r UpdateTemplateResponse) StatusCode() int { +func (r GetTagResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type GetCampaignUsersResponse struct { +type UpdateTagResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *struct { - Data []CampaignUser `json:"data"` - Limit int `json:"limit"` - Offset int `json:"offset"` - Total int `json:"total"` - } - JSONDefault *Error + JSON200 *Tag + JSONDefault *Error } // Status returns HTTPResponse.Status -func (r GetCampaignUsersResponse) Status() string { +func (r UpdateTagResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -3241,7 +3798,7 @@ func (r GetCampaignUsersResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r GetCampaignUsersResponse) StatusCode() int { +func (r UpdateTagResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } @@ -3646,6 +4203,67 @@ func (c *ClientWithResponses) GetCampaignUsersWithResponse(ctx context.Context, return ParseGetCampaignUsersResponse(rsp) } +// ListTagsWithResponse request returning *ListTagsResponse +func (c *ClientWithResponses) ListTagsWithResponse(ctx context.Context, projectID openapi_types.UUID, params *ListTagsParams, reqEditors ...RequestEditorFn) (*ListTagsResponse, error) { + rsp, err := c.ListTags(ctx, projectID, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseListTagsResponse(rsp) +} + +// CreateTagWithBodyWithResponse request with arbitrary body returning *CreateTagResponse +func (c *ClientWithResponses) CreateTagWithBodyWithResponse(ctx context.Context, projectID openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateTagResponse, error) { + rsp, err := c.CreateTagWithBody(ctx, projectID, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateTagResponse(rsp) +} + +func (c *ClientWithResponses) CreateTagWithResponse(ctx context.Context, projectID openapi_types.UUID, body CreateTagJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateTagResponse, error) { + rsp, err := c.CreateTag(ctx, projectID, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateTagResponse(rsp) +} + +// DeleteTagWithResponse request returning *DeleteTagResponse +func (c *ClientWithResponses) DeleteTagWithResponse(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, reqEditors ...RequestEditorFn) (*DeleteTagResponse, error) { + rsp, err := c.DeleteTag(ctx, projectID, tagID, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteTagResponse(rsp) +} + +// GetTagWithResponse request returning *GetTagResponse +func (c *ClientWithResponses) GetTagWithResponse(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetTagResponse, error) { + rsp, err := c.GetTag(ctx, projectID, tagID, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetTagResponse(rsp) +} + +// UpdateTagWithBodyWithResponse request with arbitrary body returning *UpdateTagResponse +func (c *ClientWithResponses) UpdateTagWithBodyWithResponse(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateTagResponse, error) { + rsp, err := c.UpdateTagWithBody(ctx, projectID, tagID, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateTagResponse(rsp) +} + +func (c *ClientWithResponses) UpdateTagWithResponse(ctx context.Context, projectID openapi_types.UUID, tagID openapi_types.UUID, body UpdateTagJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateTagResponse, error) { + rsp, err := c.UpdateTag(ctx, projectID, tagID, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateTagResponse(rsp) +} + // ListUsersWithResponse request returning *ListUsersResponse func (c *ClientWithResponses) ListUsersWithResponse(ctx context.Context, projectID openapi_types.UUID, params *ListUsersParams, reqEditors ...RequestEditorFn) (*ListUsersResponse, error) { rsp, err := c.ListUsers(ctx, projectID, params, reqEditors...) @@ -4095,7 +4713,172 @@ func ParseDuplicateCampaignResponse(rsp *http.Response) (*DuplicateCampaignRespo if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON201 = &dest + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseCreateTemplateResponse parses an HTTP response from a CreateTemplateWithResponse call +func ParseCreateTemplateResponse(rsp *http.Response) (*CreateTemplateResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreateTemplateResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest Template + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseDeleteTemplateResponse parses an HTTP response from a DeleteTemplateWithResponse call +func ParseDeleteTemplateResponse(rsp *http.Response) (*DeleteTemplateResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteTemplateResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseGetTemplateResponse parses an HTTP response from a GetTemplateWithResponse call +func ParseGetTemplateResponse(rsp *http.Response) (*GetTemplateResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetTemplateResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + Data Template `json:"data"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseUpdateTemplateResponse parses an HTTP response from a UpdateTemplateWithResponse call +func ParseUpdateTemplateResponse(rsp *http.Response) (*UpdateTemplateResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UpdateTemplateResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Template + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseGetCampaignUsersResponse parses an HTTP response from a GetCampaignUsersWithResponse call +func ParseGetCampaignUsersResponse(rsp *http.Response) (*GetCampaignUsersResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetCampaignUsersResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + Data []CampaignUser `json:"data"` + Limit int `json:"limit"` + Offset int `json:"offset"` + Total int `json:"total"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: var dest Error @@ -4109,26 +4892,26 @@ func ParseDuplicateCampaignResponse(rsp *http.Response) (*DuplicateCampaignRespo return response, nil } -// ParseCreateTemplateResponse parses an HTTP response from a CreateTemplateWithResponse call -func ParseCreateTemplateResponse(rsp *http.Response) (*CreateTemplateResponse, error) { +// ParseListTagsResponse parses an HTTP response from a ListTagsWithResponse call +func ParseListTagsResponse(rsp *http.Response) (*ListTagsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &CreateTemplateResponse{ + response := &ListTagsResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest Template + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest TagListResponse if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON201 = &dest + response.JSON200 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: var dest Error @@ -4142,20 +4925,27 @@ func ParseCreateTemplateResponse(rsp *http.Response) (*CreateTemplateResponse, e return response, nil } -// ParseDeleteTemplateResponse parses an HTTP response from a DeleteTemplateWithResponse call -func ParseDeleteTemplateResponse(rsp *http.Response) (*DeleteTemplateResponse, error) { +// ParseCreateTagResponse parses an HTTP response from a CreateTagWithResponse call +func ParseCreateTagResponse(rsp *http.Response) (*CreateTagResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DeleteTemplateResponse{ + response := &CreateTagResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest Tag + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -4168,29 +4958,20 @@ func ParseDeleteTemplateResponse(rsp *http.Response) (*DeleteTemplateResponse, e return response, nil } -// ParseGetTemplateResponse parses an HTTP response from a GetTemplateWithResponse call -func ParseGetTemplateResponse(rsp *http.Response) (*GetTemplateResponse, error) { +// ParseDeleteTagResponse parses an HTTP response from a DeleteTagWithResponse call +func ParseDeleteTagResponse(rsp *http.Response) (*DeleteTagResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetTemplateResponse{ + response := &DeleteTagResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest struct { - Data Template `json:"data"` - } - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -4203,22 +4984,22 @@ func ParseGetTemplateResponse(rsp *http.Response) (*GetTemplateResponse, error) return response, nil } -// ParseUpdateTemplateResponse parses an HTTP response from a UpdateTemplateWithResponse call -func ParseUpdateTemplateResponse(rsp *http.Response) (*UpdateTemplateResponse, error) { +// ParseGetTagResponse parses an HTTP response from a GetTagWithResponse call +func ParseGetTagResponse(rsp *http.Response) (*GetTagResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &UpdateTemplateResponse{ + response := &GetTagResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest Template + var dest Tag if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -4236,27 +5017,22 @@ func ParseUpdateTemplateResponse(rsp *http.Response) (*UpdateTemplateResponse, e return response, nil } -// ParseGetCampaignUsersResponse parses an HTTP response from a GetCampaignUsersWithResponse call -func ParseGetCampaignUsersResponse(rsp *http.Response) (*GetCampaignUsersResponse, error) { +// ParseUpdateTagResponse parses an HTTP response from a UpdateTagWithResponse call +func ParseUpdateTagResponse(rsp *http.Response) (*UpdateTagResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetCampaignUsersResponse{ + response := &UpdateTagResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest struct { - Data []CampaignUser `json:"data"` - Limit int `json:"limit"` - Offset int `json:"offset"` - Total int `json:"total"` - } + var dest Tag if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -4614,6 +5390,21 @@ type ServerInterface interface { // Get campaign users // (GET /api/admin/projects/{projectID}/campaigns/{campaignID}/users) GetCampaignUsers(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID, campaignID openapi_types.UUID, params GetCampaignUsersParams) + // List tags + // (GET /api/admin/projects/{projectID}/tags) + ListTags(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID, params ListTagsParams) + // Create tag + // (POST /api/admin/projects/{projectID}/tags) + CreateTag(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID) + // Delete tag + // (DELETE /api/admin/projects/{projectID}/tags/{tagID}) + DeleteTag(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID, tagID openapi_types.UUID) + // Get tag by ID + // (GET /api/admin/projects/{projectID}/tags/{tagID}) + GetTag(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID, tagID openapi_types.UUID) + // Update tag + // (PATCH /api/admin/projects/{projectID}/tags/{tagID}) + UpdateTag(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID, tagID openapi_types.UUID) // List users // (GET /api/admin/projects/{projectID}/users) ListUsers(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID, params ListUsersParams) @@ -4743,6 +5534,36 @@ func (_ Unimplemented) GetCampaignUsers(w http.ResponseWriter, r *http.Request, w.WriteHeader(http.StatusNotImplemented) } +// List tags +// (GET /api/admin/projects/{projectID}/tags) +func (_ Unimplemented) ListTags(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID, params ListTagsParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Create tag +// (POST /api/admin/projects/{projectID}/tags) +func (_ Unimplemented) CreateTag(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Delete tag +// (DELETE /api/admin/projects/{projectID}/tags/{tagID}) +func (_ Unimplemented) DeleteTag(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID, tagID openapi_types.UUID) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Get tag by ID +// (GET /api/admin/projects/{projectID}/tags/{tagID}) +func (_ Unimplemented) GetTag(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID, tagID openapi_types.UUID) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Update tag +// (PATCH /api/admin/projects/{projectID}/tags/{tagID}) +func (_ Unimplemented) UpdateTag(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID, tagID openapi_types.UUID) { + w.WriteHeader(http.StatusNotImplemented) +} + // List users // (GET /api/admin/projects/{projectID}/users) func (_ Unimplemented) ListUsers(w http.ResponseWriter, r *http.Request, projectID openapi_types.UUID, params ListUsersParams) { @@ -5435,6 +6256,215 @@ func (siw *ServerInterfaceWrapper) GetCampaignUsers(w http.ResponseWriter, r *ht handler.ServeHTTP(w, r) } +// ListTags operation middleware +func (siw *ServerInterfaceWrapper) ListTags(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "projectID" ------------- + var projectID openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "projectID", chi.URLParam(r, "projectID"), &projectID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectID", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, HttpBearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params ListTagsParams + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", r.URL.Query(), ¶ms.Limit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + // ------------- Optional query parameter "offset" ------------- + + err = runtime.BindQueryParameter("form", true, false, "offset", r.URL.Query(), ¶ms.Offset) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err}) + return + } + + // ------------- Optional query parameter "search" ------------- + + err = runtime.BindQueryParameter("form", true, false, "search", r.URL.Query(), ¶ms.Search) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "search", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListTags(w, r, projectID, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// CreateTag operation middleware +func (siw *ServerInterfaceWrapper) CreateTag(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "projectID" ------------- + var projectID openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "projectID", chi.URLParam(r, "projectID"), &projectID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectID", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, HttpBearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CreateTag(w, r, projectID) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// DeleteTag operation middleware +func (siw *ServerInterfaceWrapper) DeleteTag(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "projectID" ------------- + var projectID openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "projectID", chi.URLParam(r, "projectID"), &projectID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectID", Err: err}) + return + } + + // ------------- Path parameter "tagID" ------------- + var tagID openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "tagID", chi.URLParam(r, "tagID"), &tagID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tagID", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, HttpBearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeleteTag(w, r, projectID, tagID) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetTag operation middleware +func (siw *ServerInterfaceWrapper) GetTag(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "projectID" ------------- + var projectID openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "projectID", chi.URLParam(r, "projectID"), &projectID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectID", Err: err}) + return + } + + // ------------- Path parameter "tagID" ------------- + var tagID openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "tagID", chi.URLParam(r, "tagID"), &tagID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tagID", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, HttpBearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetTag(w, r, projectID, tagID) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// UpdateTag operation middleware +func (siw *ServerInterfaceWrapper) UpdateTag(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "projectID" ------------- + var projectID openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "projectID", chi.URLParam(r, "projectID"), &projectID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectID", Err: err}) + return + } + + // ------------- Path parameter "tagID" ------------- + var tagID openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "tagID", chi.URLParam(r, "tagID"), &tagID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tagID", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, HttpBearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.UpdateTag(w, r, projectID, tagID) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ListUsers operation middleware func (siw *ServerInterfaceWrapper) ListUsers(w http.ResponseWriter, r *http.Request) { @@ -6022,6 +7052,21 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/admin/projects/{projectID}/campaigns/{campaignID}/users", wrapper.GetCampaignUsers) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/api/admin/projects/{projectID}/tags", wrapper.ListTags) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/api/admin/projects/{projectID}/tags", wrapper.CreateTag) + }) + r.Group(func(r chi.Router) { + r.Delete(options.BaseURL+"/api/admin/projects/{projectID}/tags/{tagID}", wrapper.DeleteTag) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/api/admin/projects/{projectID}/tags/{tagID}", wrapper.GetTag) + }) + r.Group(func(r chi.Router) { + r.Patch(options.BaseURL+"/api/admin/projects/{projectID}/tags/{tagID}", wrapper.UpdateTag) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/admin/projects/{projectID}/users", wrapper.ListUsers) }) From aed8353aa33866a6454670a7bb2f0caf0fd2bfd7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 20:12:07 +0000 Subject: [PATCH 3/7] Address code review feedback: improve SQL injection protection and optimize database queries Co-authored-by: jeroenrinzema <3440116+jeroenrinzema@users.noreply.github.com> --- .../internal/http/controllers/v1/tags.go | 18 +------ services/nexus/internal/store/tags.go | 47 +++++++++++++++++-- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/services/nexus/internal/http/controllers/v1/tags.go b/services/nexus/internal/http/controllers/v1/tags.go index cf9a81ee..f0a794a9 100644 --- a/services/nexus/internal/http/controllers/v1/tags.go +++ b/services/nexus/internal/http/controllers/v1/tags.go @@ -137,20 +137,13 @@ func (srv *TagsController) UpdateTag(w http.ResponseWriter, r *http.Request, pro return } - _, err = srv.store.TagsStore.GetTag(ctx, projectID, tagID) + err = srv.store.TagsStore.UpdateTag(ctx, projectID, tagID, body.Name) if errors.Is(err, sql.ErrNoRows) { logger.Error("tag not found", zap.Stringer("tag_id", tagID)) oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("tag not found"))) return } - if err != nil { - logger.Error("failed to get tag", zap.Error(err)) - oapi.WriteProblem(w, err) - return - } - - err = srv.store.TagsStore.UpdateTag(ctx, projectID, tagID, body.Name) if err != nil { logger.Error("failed to update tag", zap.Error(err)) oapi.WriteProblem(w, err) @@ -173,20 +166,13 @@ func (srv *TagsController) DeleteTag(w http.ResponseWriter, r *http.Request, pro logger := srv.logger.With(zap.Stringer("project_id", projectID), zap.Stringer("tag_id", tagID)) logger.Info("deleting tag") - _, err := srv.store.TagsStore.GetTag(ctx, projectID, tagID) + err := srv.store.TagsStore.DeleteTag(ctx, projectID, tagID) if errors.Is(err, sql.ErrNoRows) { logger.Error("tag not found", zap.Stringer("tag_id", tagID)) oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("tag not found"))) return } - if err != nil { - logger.Error("failed to get tag", zap.Error(err)) - oapi.WriteProblem(w, err) - return - } - - err = srv.store.TagsStore.DeleteTag(ctx, projectID, tagID) if err != nil { logger.Error("failed to delete tag", zap.Error(err)) oapi.WriteProblem(w, err) diff --git a/services/nexus/internal/store/tags.go b/services/nexus/internal/store/tags.go index d6b6bb74..f0ec401e 100644 --- a/services/nexus/internal/store/tags.go +++ b/services/nexus/internal/store/tags.go @@ -2,6 +2,7 @@ package store import ( "context" + "strings" "time" "github.com/google/uuid" @@ -63,6 +64,9 @@ func (s *TagsStore) CreateTag(ctx context.Context, projectID uuid.UUID, name str } func (s *TagsStore) ListTags(ctx context.Context, projectID uuid.UUID, pagination Pagination, search string) (Tags, int, error) { + // Escape special ILIKE pattern characters to prevent SQL injection + search = escapeILIKEPattern(search) + query := ` SELECT id, project_id, name, created_at, updated_at, COUNT(*) OVER () AS total_count @@ -94,6 +98,15 @@ func (s *TagsStore) ListTags(ctx context.Context, projectID uuid.UUID, paginatio return tags, total, nil } +// escapeILIKEPattern escapes special ILIKE pattern characters to prevent SQL injection +func escapeILIKEPattern(s string) string { + // Escape backslash first, then % and _ + s = strings.ReplaceAll(s, "\\", "\\\\") + s = strings.ReplaceAll(s, "%", "\\%") + s = strings.ReplaceAll(s, "_", "\\_") + return s +} + func (s *TagsStore) GetTag(ctx context.Context, projectID, tagID uuid.UUID) (*Tag, error) { query := ` SELECT id, project_id, name, created_at, updated_at @@ -117,8 +130,21 @@ func (s *TagsStore) UpdateTag(ctx context.Context, projectID, tagID uuid.UUID, n WHERE project_id = $2 AND id = $3` - _, err := s.db.ExecContext(ctx, stmt, name, projectID, tagID) - return err + result, err := s.db.ExecContext(ctx, stmt, name, projectID, tagID) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + return ErrNoRows + } + + return nil } func (s *TagsStore) DeleteTag(ctx context.Context, projectID, tagID uuid.UUID) error { @@ -127,6 +153,19 @@ func (s *TagsStore) DeleteTag(ctx context.Context, projectID, tagID uuid.UUID) e WHERE project_id = $1 AND id = $2` - _, err := s.db.ExecContext(ctx, stmt, projectID, tagID) - return err + result, err := s.db.ExecContext(ctx, stmt, projectID, tagID) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + return ErrNoRows + } + + return nil } From 4114c8e4288845321675288c61618917a2349edf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 20:15:12 +0000 Subject: [PATCH 4/7] Use store.ErrNoRows consistently throughout tags controller Co-authored-by: jeroenrinzema <3440116+jeroenrinzema@users.noreply.github.com> --- services/nexus/internal/http/controllers/v1/tags.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/services/nexus/internal/http/controllers/v1/tags.go b/services/nexus/internal/http/controllers/v1/tags.go index f0a794a9..e95c25a7 100644 --- a/services/nexus/internal/http/controllers/v1/tags.go +++ b/services/nexus/internal/http/controllers/v1/tags.go @@ -1,7 +1,6 @@ package v1 import ( - "database/sql" "errors" "net/http" @@ -42,7 +41,7 @@ func (srv *TagsController) CreateTag(w http.ResponseWriter, r *http.Request, pro // Check if project exists _, err = srv.store.ProjectsStore.GetProject(ctx, projectID) - if errors.Is(err, sql.ErrNoRows) { + if errors.Is(err, store.ErrNoRows) { logger.Error("project not found", zap.Stringer("project_id", projectID)) oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("project not found"))) return @@ -109,7 +108,7 @@ func (srv *TagsController) GetTag(w http.ResponseWriter, r *http.Request, projec logger.Info("getting tag") tag, err := srv.store.TagsStore.GetTag(ctx, projectID, tagID) - if errors.Is(err, sql.ErrNoRows) { + if errors.Is(err, store.ErrNoRows) { logger.Error("tag not found", zap.Stringer("tag_id", tagID)) oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("tag not found"))) return @@ -138,7 +137,7 @@ func (srv *TagsController) UpdateTag(w http.ResponseWriter, r *http.Request, pro } err = srv.store.TagsStore.UpdateTag(ctx, projectID, tagID, body.Name) - if errors.Is(err, sql.ErrNoRows) { + if errors.Is(err, store.ErrNoRows) { logger.Error("tag not found", zap.Stringer("tag_id", tagID)) oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("tag not found"))) return @@ -167,7 +166,7 @@ func (srv *TagsController) DeleteTag(w http.ResponseWriter, r *http.Request, pro logger.Info("deleting tag") err := srv.store.TagsStore.DeleteTag(ctx, projectID, tagID) - if errors.Is(err, sql.ErrNoRows) { + if errors.Is(err, store.ErrNoRows) { logger.Error("tag not found", zap.Stringer("tag_id", tagID)) oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("tag not found"))) return From 6495ae0c631b3a065ce728a5666582354431515d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 20:39:12 +0000 Subject: [PATCH 5/7] Address PR review comments: add soft deletes, fix ILIKE comment, remove unnecessary project check Co-authored-by: jeroenrinzema <3440116+jeroenrinzema@users.noreply.github.com> --- .../nexus/internal/http/controllers/v1/tags.go | 14 -------------- .../1764534970_add_tags_deleted_at.down.sql | 1 + .../1764534970_add_tags_deleted_at.up.sql | 1 + services/nexus/internal/store/tags.go | 16 ++++++++++------ 4 files changed, 12 insertions(+), 20 deletions(-) create mode 100644 services/nexus/internal/store/migrations/1764534970_add_tags_deleted_at.down.sql create mode 100644 services/nexus/internal/store/migrations/1764534970_add_tags_deleted_at.up.sql diff --git a/services/nexus/internal/http/controllers/v1/tags.go b/services/nexus/internal/http/controllers/v1/tags.go index e95c25a7..4650bcde 100644 --- a/services/nexus/internal/http/controllers/v1/tags.go +++ b/services/nexus/internal/http/controllers/v1/tags.go @@ -39,20 +39,6 @@ func (srv *TagsController) CreateTag(w http.ResponseWriter, r *http.Request, pro logger := srv.logger.With(zap.Stringer("project_id", projectID), zap.String("name", body.Name)) logger.Info("creating tag") - // Check if project exists - _, err = srv.store.ProjectsStore.GetProject(ctx, projectID) - if errors.Is(err, store.ErrNoRows) { - logger.Error("project not found", zap.Stringer("project_id", projectID)) - oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("project not found"))) - return - } - - if err != nil { - logger.Error("failed to get project", zap.Error(err)) - oapi.WriteProblem(w, err) - return - } - tagID, err := srv.store.TagsStore.CreateTag(ctx, projectID, body.Name) if err != nil { logger.Error("failed to create tag", zap.Error(err)) diff --git a/services/nexus/internal/store/migrations/1764534970_add_tags_deleted_at.down.sql b/services/nexus/internal/store/migrations/1764534970_add_tags_deleted_at.down.sql new file mode 100644 index 00000000..b167d795 --- /dev/null +++ b/services/nexus/internal/store/migrations/1764534970_add_tags_deleted_at.down.sql @@ -0,0 +1 @@ +ALTER TABLE tags DROP COLUMN deleted_at; diff --git a/services/nexus/internal/store/migrations/1764534970_add_tags_deleted_at.up.sql b/services/nexus/internal/store/migrations/1764534970_add_tags_deleted_at.up.sql new file mode 100644 index 00000000..6ce3af6a --- /dev/null +++ b/services/nexus/internal/store/migrations/1764534970_add_tags_deleted_at.up.sql @@ -0,0 +1 @@ +ALTER TABLE tags ADD COLUMN deleted_at timestamptz; diff --git a/services/nexus/internal/store/tags.go b/services/nexus/internal/store/tags.go index f0ec401e..dbefa28e 100644 --- a/services/nexus/internal/store/tags.go +++ b/services/nexus/internal/store/tags.go @@ -64,14 +64,15 @@ func (s *TagsStore) CreateTag(ctx context.Context, projectID uuid.UUID, name str } func (s *TagsStore) ListTags(ctx context.Context, projectID uuid.UUID, pagination Pagination, search string) (Tags, int, error) { - // Escape special ILIKE pattern characters to prevent SQL injection + // Escape special ILIKE pattern characters to treat them as literals search = escapeILIKEPattern(search) query := ` - SELECT id, project_id, name, created_at, updated_at, + SELECT id, project_id, name, created_at, updated_at, deleted_at, COUNT(*) OVER () AS total_count FROM tags WHERE project_id = $1 + AND deleted_at IS NULL AND ($2 = '' OR name ILIKE '%' || $2 || '%') ORDER BY name ASC LIMIT $3 OFFSET $4` @@ -109,10 +110,11 @@ func escapeILIKEPattern(s string) string { func (s *TagsStore) GetTag(ctx context.Context, projectID, tagID uuid.UUID) (*Tag, error) { query := ` - SELECT id, project_id, name, created_at, updated_at + SELECT id, project_id, name, created_at, updated_at, deleted_at FROM tags WHERE project_id = $1 - AND id = $2` + AND id = $2 + AND deleted_at IS NULL` var tag Tag err := s.db.GetContext(ctx, &tag, query, projectID, tagID) @@ -149,9 +151,11 @@ func (s *TagsStore) UpdateTag(ctx context.Context, projectID, tagID uuid.UUID, n func (s *TagsStore) DeleteTag(ctx context.Context, projectID, tagID uuid.UUID) error { stmt := ` - DELETE FROM tags + UPDATE tags + SET deleted_at = NOW() WHERE project_id = $1 - AND id = $2` + AND id = $2 + AND deleted_at IS NULL` result, err := s.db.ExecContext(ctx, stmt, projectID, tagID) if err != nil { From 9e867da4a4bc07f09494c62e83e1129861396d58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 20:49:47 +0000 Subject: [PATCH 6/7] Fix UpdateTag to filter soft-deleted tags and correct escapeILIKEPattern comment Co-authored-by: jeroenrinzema <3440116+jeroenrinzema@users.noreply.github.com> --- services/nexus/internal/store/tags.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/nexus/internal/store/tags.go b/services/nexus/internal/store/tags.go index dbefa28e..a34e861b 100644 --- a/services/nexus/internal/store/tags.go +++ b/services/nexus/internal/store/tags.go @@ -99,7 +99,8 @@ func (s *TagsStore) ListTags(ctx context.Context, projectID uuid.UUID, paginatio return tags, total, nil } -// escapeILIKEPattern escapes special ILIKE pattern characters to prevent SQL injection +// escapeILIKEPattern escapes special ILIKE pattern characters (% and _) so that user input is treated as a literal in ILIKE queries. +// This prevents unintended wildcard matching, not SQL injection. SQL injection is already prevented by using parameterized queries. func escapeILIKEPattern(s string) string { // Escape backslash first, then % and _ s = strings.ReplaceAll(s, "\\", "\\\\") @@ -130,7 +131,8 @@ func (s *TagsStore) UpdateTag(ctx context.Context, projectID, tagID uuid.UUID, n UPDATE tags SET name = $1 WHERE project_id = $2 - AND id = $3` + AND id = $3 + AND deleted_at IS NULL` result, err := s.db.ExecContext(ctx, stmt, name, projectID, tagID) if err != nil { From 215177eac032eea230fe362137264233493ed0a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 21:15:10 +0000 Subject: [PATCH 7/7] Simplify tags implementation: remove pattern escaping and RowsAffected checks Co-authored-by: jeroenrinzema <3440116+jeroenrinzema@users.noreply.github.com> --- .../internal/http/controllers/v1/tags.go | 25 +++------- .../internal/http/controllers/v1/tags_test.go | 4 +- services/nexus/internal/store/tags.go | 48 ++----------------- 3 files changed, 13 insertions(+), 64 deletions(-) diff --git a/services/nexus/internal/http/controllers/v1/tags.go b/services/nexus/internal/http/controllers/v1/tags.go index 4650bcde..05076e37 100644 --- a/services/nexus/internal/http/controllers/v1/tags.go +++ b/services/nexus/internal/http/controllers/v1/tags.go @@ -67,12 +67,7 @@ func (srv *TagsController) ListTags(w http.ResponseWriter, r *http.Request, proj Offset: params.Offset.ToInt(), } - search := "" - if params.Search != nil { - search = params.Search.ToString() - } - - result, total, err := srv.store.TagsStore.ListTags(ctx, projectID, pagination, search) + result, total, err := srv.store.TagsStore.ListTags(ctx, projectID, pagination, params.Search.ToString()) if err != nil { logger.Error("failed to list tags", zap.Error(err)) oapi.WriteProblem(w, err) @@ -123,12 +118,6 @@ func (srv *TagsController) UpdateTag(w http.ResponseWriter, r *http.Request, pro } err = srv.store.TagsStore.UpdateTag(ctx, projectID, tagID, body.Name) - if errors.Is(err, store.ErrNoRows) { - logger.Error("tag not found", zap.Stringer("tag_id", tagID)) - oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("tag not found"))) - return - } - if err != nil { logger.Error("failed to update tag", zap.Error(err)) oapi.WriteProblem(w, err) @@ -136,6 +125,12 @@ func (srv *TagsController) UpdateTag(w http.ResponseWriter, r *http.Request, pro } tag, err := srv.store.TagsStore.GetTag(ctx, projectID, tagID) + if errors.Is(err, store.ErrNoRows) { + logger.Error("tag not found", zap.Stringer("tag_id", tagID)) + oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("tag not found"))) + return + } + if err != nil { logger.Error("failed to fetch updated tag", zap.Error(err)) oapi.WriteProblem(w, err) @@ -152,12 +147,6 @@ func (srv *TagsController) DeleteTag(w http.ResponseWriter, r *http.Request, pro logger.Info("deleting tag") err := srv.store.TagsStore.DeleteTag(ctx, projectID, tagID) - if errors.Is(err, store.ErrNoRows) { - logger.Error("tag not found", zap.Stringer("tag_id", tagID)) - oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("tag not found"))) - return - } - if err != nil { logger.Error("failed to delete tag", zap.Error(err)) oapi.WriteProblem(w, err) diff --git a/services/nexus/internal/http/controllers/v1/tags_test.go b/services/nexus/internal/http/controllers/v1/tags_test.go index 72a54b45..257641ff 100644 --- a/services/nexus/internal/http/controllers/v1/tags_test.go +++ b/services/nexus/internal/http/controllers/v1/tags_test.go @@ -262,7 +262,7 @@ func TestUpdateTag(t *testing.T) { body: oapi.UpdateTagJSONRequestBody{ Name: "another-name", }, - code: 404, + code: 404, // GetTag will return 404 for non-existent tag }, } @@ -324,7 +324,7 @@ func TestDeleteTag(t *testing.T) { }, "non-existing-tag": { tagID: uuid.New(), - code: 404, + code: 204, // Succeeds even if tag doesn't exist (idempotent) }, } diff --git a/services/nexus/internal/store/tags.go b/services/nexus/internal/store/tags.go index a34e861b..4dcdb60d 100644 --- a/services/nexus/internal/store/tags.go +++ b/services/nexus/internal/store/tags.go @@ -2,7 +2,6 @@ package store import ( "context" - "strings" "time" "github.com/google/uuid" @@ -64,9 +63,6 @@ func (s *TagsStore) CreateTag(ctx context.Context, projectID uuid.UUID, name str } func (s *TagsStore) ListTags(ctx context.Context, projectID uuid.UUID, pagination Pagination, search string) (Tags, int, error) { - // Escape special ILIKE pattern characters to treat them as literals - search = escapeILIKEPattern(search) - query := ` SELECT id, project_id, name, created_at, updated_at, deleted_at, COUNT(*) OVER () AS total_count @@ -99,16 +95,6 @@ func (s *TagsStore) ListTags(ctx context.Context, projectID uuid.UUID, paginatio return tags, total, nil } -// escapeILIKEPattern escapes special ILIKE pattern characters (% and _) so that user input is treated as a literal in ILIKE queries. -// This prevents unintended wildcard matching, not SQL injection. SQL injection is already prevented by using parameterized queries. -func escapeILIKEPattern(s string) string { - // Escape backslash first, then % and _ - s = strings.ReplaceAll(s, "\\", "\\\\") - s = strings.ReplaceAll(s, "%", "\\%") - s = strings.ReplaceAll(s, "_", "\\_") - return s -} - func (s *TagsStore) GetTag(ctx context.Context, projectID, tagID uuid.UUID) (*Tag, error) { query := ` SELECT id, project_id, name, created_at, updated_at, deleted_at @@ -134,21 +120,8 @@ func (s *TagsStore) UpdateTag(ctx context.Context, projectID, tagID uuid.UUID, n AND id = $3 AND deleted_at IS NULL` - result, err := s.db.ExecContext(ctx, stmt, name, projectID, tagID) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - - if rowsAffected == 0 { - return ErrNoRows - } - - return nil + _, err := s.db.ExecContext(ctx, stmt, name, projectID, tagID) + return err } func (s *TagsStore) DeleteTag(ctx context.Context, projectID, tagID uuid.UUID) error { @@ -159,19 +132,6 @@ func (s *TagsStore) DeleteTag(ctx context.Context, projectID, tagID uuid.UUID) e AND id = $2 AND deleted_at IS NULL` - result, err := s.db.ExecContext(ctx, stmt, projectID, tagID) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - - if rowsAffected == 0 { - return ErrNoRows - } - - return nil + _, err := s.db.ExecContext(ctx, stmt, projectID, tagID) + return err }