-
-
Notifications
You must be signed in to change notification settings - Fork 55
feat: extensions #430
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: extensions #430
Changes from all commits
bc2819d
798fef9
5d302ac
340ff69
a0c64aa
12661f0
9a5e87a
52d3331
882c741
a61334f
6867251
1c1485b
9572671
76238a7
7fcee25
808cad0
7b4020b
3b03319
4c3db53
9c89d61
fac665f
9558b28
1bd2fa0
dab77db
0e5d58d
e88d011
9e490dc
54c82ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package controller | ||
|
||
import ( | ||
"net/http" | ||
|
||
"github.com/go-fuego/fuego" | ||
"github.com/raghavyuva/nixopus-api/internal/features/logger" | ||
) | ||
|
||
func (c *ExtensionsController) DeleteFork(ctx fuego.ContextNoBody) (*struct { | ||
Status string `json:"status"` | ||
}, error) { | ||
id := ctx.PathParam("id") | ||
if id == "" { | ||
return nil, fuego.HTTPError{Err: nil, Status: http.StatusBadRequest} | ||
} | ||
if err := c.service.DeleteFork(id); err != nil { | ||
c.logger.Log(logger.Error, err.Error(), "") | ||
return nil, fuego.HTTPError{Err: err, Status: http.StatusBadRequest} | ||
} | ||
return &struct { | ||
Status string `json:"status"` | ||
}{Status: "ok"}, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package controller | ||
|
||
import ( | ||
"net/http" | ||
|
||
"github.com/go-fuego/fuego" | ||
"github.com/raghavyuva/nixopus-api/internal/features/logger" | ||
"github.com/raghavyuva/nixopus-api/internal/types" | ||
) | ||
|
||
type ForkExtensionRequest struct { | ||
YAMLContent *string `json:"yaml_content"` | ||
} | ||
|
||
func (c *ExtensionsController) ForkExtension(ctx fuego.ContextWithBody[ForkExtensionRequest]) (types.Extension, error) { | ||
extensionID := ctx.PathParam("extension_id") | ||
if extensionID == "" { | ||
return types.Extension{}, fuego.HTTPError{Err: nil, Status: http.StatusBadRequest} | ||
} | ||
req, err := ctx.Body() | ||
if err != nil { | ||
return types.Extension{}, fuego.HTTPError{Err: err, Status: http.StatusBadRequest} | ||
} | ||
var yamlOverride string | ||
if req.YAMLContent != nil { | ||
yamlOverride = *req.YAMLContent | ||
} | ||
authorName := "" | ||
if userAny := ctx.Request().Context().Value(types.UserContextKey); userAny != nil { | ||
if u, ok := userAny.(*types.User); ok && u != nil { | ||
authorName = u.Username | ||
} | ||
} | ||
newExt, err := c.service.ForkExtension(extensionID, yamlOverride, authorName) | ||
if err != nil { | ||
c.logger.Log(logger.Error, err.Error(), "") | ||
return types.Extension{}, fuego.HTTPError{Err: err, Status: http.StatusInternalServerError} | ||
} | ||
return *newExt, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package controller | ||
|
||
import ( | ||
"net/http" | ||
"strconv" | ||
|
||
"github.com/go-fuego/fuego" | ||
"github.com/raghavyuva/nixopus-api/internal/features/logger" | ||
"github.com/raghavyuva/nixopus-api/internal/types" | ||
) | ||
|
||
func (c *ExtensionsController) GetExtensions(ctx fuego.ContextNoBody) (*types.ExtensionListResponse, error) { | ||
params := types.ExtensionListParams{} | ||
|
||
categoryParam := ctx.QueryParam("category") | ||
if categoryParam != "" { | ||
cat := types.ExtensionCategory(categoryParam) | ||
params.Category = &cat | ||
} | ||
|
||
searchParam := ctx.QueryParam("search") | ||
if searchParam != "" { | ||
params.Search = searchParam | ||
} | ||
|
||
if typeParam := ctx.QueryParam("type"); typeParam != "" { | ||
et := types.ExtensionType(typeParam) | ||
params.Type = &et | ||
} | ||
|
||
sortByParam := ctx.QueryParam("sort_by") | ||
if sortByParam != "" { | ||
params.SortBy = types.ExtensionSortField(sortByParam) | ||
} | ||
|
||
sortDirParam := ctx.QueryParam("sort_dir") | ||
if sortDirParam != "" { | ||
params.SortDir = types.SortDirection(sortDirParam) | ||
} | ||
|
||
pageParam := ctx.QueryParam("page") | ||
if pageParam != "" { | ||
if page, err := strconv.Atoi(pageParam); err == nil && page > 0 { | ||
params.Page = page | ||
} | ||
} | ||
|
||
pageSizeParam := ctx.QueryParam("page_size") | ||
if pageSizeParam != "" { | ||
if pageSize, err := strconv.Atoi(pageSizeParam); err == nil && pageSize > 0 { | ||
params.PageSize = pageSize | ||
} | ||
} | ||
|
||
response, err := c.service.ListExtensions(params) | ||
if err != nil { | ||
c.logger.Log(logger.Error, err.Error(), "") | ||
return nil, fuego.HTTPError{ | ||
Err: err, | ||
Status: http.StatusInternalServerError, | ||
} | ||
} | ||
|
||
return response, nil | ||
} | ||
|
||
func (c *ExtensionsController) GetCategories(ctx fuego.ContextNoBody) ([]types.ExtensionCategory, error) { | ||
cats, err := c.service.ListCategories() | ||
if err != nil { | ||
c.logger.Log(logger.Error, err.Error(), "") | ||
return nil, fuego.HTTPError{Err: err, Status: http.StatusInternalServerError} | ||
} | ||
return cats, nil | ||
} | ||
|
||
func (c *ExtensionsController) GetExtension(ctx fuego.ContextNoBody) (types.Extension, error) { | ||
id := ctx.PathParam("id") | ||
if id == "" { | ||
return types.Extension{}, fuego.HTTPError{ | ||
Err: nil, | ||
Status: http.StatusBadRequest, | ||
} | ||
} | ||
Comment on lines
+77
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bad request responses should include an error message. Nil Err degrades API ergonomics. - if id == "" {
- return types.Extension{}, fuego.HTTPError{
- Err: nil,
- Status: http.StatusBadRequest,
- }
- }
+ if id == "" {
+ return types.Extension{}, fuego.HTTPError{
+ Err: fmt.Errorf("missing path parameter: id"),
+ Status: http.StatusBadRequest,
+ }
+ } Apply similarly in Lines 95-101, 123-128, and 143-148 for extension_id/execution_id. 🤖 Prompt for AI Agents
|
||
|
||
extension, err := c.service.GetExtension(id) | ||
if err != nil { | ||
if err.Error() == "extension not found" { | ||
return types.Extension{}, fuego.HTTPError{ | ||
Err: err, | ||
Status: http.StatusNotFound, | ||
} | ||
} | ||
Comment on lines
+86
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Avoid brittle error detection with string comparison. Line 87 uses string comparison to detect "extension not found" errors. This is fragile and will break if the error message changes. Consider using sentinel errors or custom error types for better error handling. Example approach: // In types/extension.go or errors.go
var ErrExtensionNotFound = errors.New("extension not found")
// In service layer, return:
return nil, ErrExtensionNotFound
// In controller, check:
if errors.Is(err, types.ErrExtensionNotFound) {
return types.Extension{}, fuego.HTTPError{
Err: err,
Status: http.StatusNotFound,
}
} 🤖 Prompt for AI Agents
|
||
c.logger.Log(logger.Error, err.Error(), "") | ||
return types.Extension{}, fuego.HTTPError{ | ||
Err: err, | ||
Status: http.StatusInternalServerError, | ||
} | ||
} | ||
|
||
return *extension, nil | ||
} | ||
|
||
func (c *ExtensionsController) GetExtensionByExtensionID(ctx fuego.ContextNoBody) (types.Extension, error) { | ||
extensionID := ctx.PathParam("extension_id") | ||
if extensionID == "" { | ||
return types.Extension{}, fuego.HTTPError{ | ||
Err: nil, | ||
Status: http.StatusBadRequest, | ||
} | ||
} | ||
|
||
extension, err := c.service.GetExtensionByID(extensionID) | ||
if err != nil { | ||
if err.Error() == "extension not found" { | ||
return types.Extension{}, fuego.HTTPError{ | ||
Err: err, | ||
Status: http.StatusNotFound, | ||
} | ||
} | ||
c.logger.Log(logger.Error, err.Error(), "") | ||
return types.Extension{}, fuego.HTTPError{ | ||
Err: err, | ||
Status: http.StatusInternalServerError, | ||
} | ||
} | ||
|
||
return *extension, nil | ||
} | ||
|
||
func (c *ExtensionsController) GetExecution(ctx fuego.ContextNoBody) (*types.ExtensionExecution, error) { | ||
id := ctx.PathParam("execution_id") | ||
if id == "" { | ||
return nil, fuego.HTTPError{ | ||
Err: nil, | ||
Status: http.StatusBadRequest, | ||
} | ||
} | ||
|
||
exec, err := c.service.GetExecutionByID(id) | ||
if err != nil { | ||
c.logger.Log(logger.Error, err.Error(), "") | ||
return nil, fuego.HTTPError{ | ||
Err: err, | ||
Status: http.StatusInternalServerError, | ||
} | ||
} | ||
return exec, nil | ||
} | ||
|
||
func (c *ExtensionsController) ListExecutionsByExtensionID(ctx fuego.ContextNoBody) ([]types.ExtensionExecution, error) { | ||
extensionID := ctx.PathParam("extension_id") | ||
if extensionID == "" { | ||
return nil, fuego.HTTPError{ | ||
Err: nil, | ||
Status: http.StatusBadRequest, | ||
} | ||
} | ||
execs, err := c.service.ListExecutionsByExtensionID(extensionID) | ||
if err != nil { | ||
c.logger.Log(logger.Error, err.Error(), "") | ||
return nil, fuego.HTTPError{ | ||
Err: err, | ||
Status: http.StatusInternalServerError, | ||
} | ||
} | ||
return execs, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package controller | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
|
||
"github.com/raghavyuva/nixopus-api/internal/features/extension/service" | ||
"github.com/raghavyuva/nixopus-api/internal/features/extension/storage" | ||
"github.com/raghavyuva/nixopus-api/internal/features/extension/validation" | ||
"github.com/raghavyuva/nixopus-api/internal/features/logger" | ||
shared_storage "github.com/raghavyuva/nixopus-api/internal/storage" | ||
"github.com/raghavyuva/nixopus-api/internal/utils" | ||
) | ||
|
||
type ExtensionsController struct { | ||
store *shared_storage.Store | ||
service *service.ExtensionService | ||
validator *validation.Validator | ||
ctx context.Context | ||
logger logger.Logger | ||
} | ||
|
||
func NewExtensionsController( | ||
store *shared_storage.Store, | ||
ctx context.Context, | ||
l logger.Logger, | ||
) *ExtensionsController { | ||
storage := storage.ExtensionStorage{DB: store.DB, Ctx: ctx} | ||
return &ExtensionsController{ | ||
store: store, | ||
service: service.NewExtensionService(store, ctx, l, &storage), | ||
validator: validation.NewValidator(&storage), | ||
ctx: ctx, | ||
logger: l, | ||
} | ||
} | ||
|
||
// parseAndValidate parses and validates the request body. | ||
// | ||
// This method attempts to parse the request body into the provided 'req' interface | ||
// using the controller's validator. If parsing fails, an error response is sent | ||
// and the method returns false. It also validates the parsed request object and | ||
// returns false if validation fails. If both operations are successful, it returns true. | ||
// | ||
// Parameters: | ||
// | ||
// w - the HTTP response writer to send error responses. | ||
// r - the HTTP request containing the body to parse. | ||
// req - the interface to populate with the parsed request body. | ||
// | ||
// Returns: | ||
// | ||
// bool - true if parsing and validation succeed, false otherwise. | ||
func (c *ExtensionsController) parseAndValidate(w http.ResponseWriter, r *http.Request, req interface{}) bool { | ||
if err := c.validator.ParseRequestBody(r, r.Body, req); err != nil { | ||
c.logger.Log(logger.Error, "Failed to decode request", err.Error()) | ||
utils.SendErrorResponse(w, "Failed to decode request", http.StatusBadRequest) | ||
return false | ||
} | ||
|
||
if err := c.validator.ValidateRequest(req); err != nil { | ||
c.logger.Log(logger.Error, err.Error(), err.Error()) | ||
utils.SendErrorResponse(w, err.Error(), http.StatusBadRequest) | ||
return false | ||
} | ||
|
||
return true | ||
} | ||
|
||
type RunExtensionRequest struct { | ||
Variables map[string]interface{} `json:"variables"` | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return meaningful errors and proper status codes
Apply this diff:
Also applies to: 15-20
🤖 Prompt for AI Agents