From 4978bbc676f5be3603c3ceaaf0b094885dfdf23b Mon Sep 17 00:00:00 2001 From: Mathieu Coulet Date: Thu, 5 Dec 2024 22:41:21 +0100 Subject: [PATCH] feat(about): load at startup service from file into db and give about.json as templated --- server/docs/docs.go | 131 ++++++++++++++++++++++-- server/docs/swagger.json | 131 ++++++++++++++++++++++-- server/docs/swagger.yaml | 87 ++++++++++++++-- server/internal/controllers/about.go | 81 +++++++++++++-- server/internal/controllers/workflow.go | 9 +- server/internal/models/about.go | 14 +-- server/internal/models/apiDTO.go | 25 +++++ server/internal/models/service.go | 2 +- server/internal/pkg/about.go | 87 ++++++++++++++-- server/internal/pkg/db.go | 2 +- server/main.go | 4 + 11 files changed, 520 insertions(+), 53 deletions(-) create mode 100644 server/internal/models/apiDTO.go diff --git a/server/docs/docs.go b/server/docs/docs.go index e414698..85b3dad 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -26,7 +26,12 @@ const docTemplate = `{ "paths": { "/about.json": { "get": { - "description": "about", + "security": [ + { + "Bearer": [] + } + ], + "description": "Get information about the server", "consumes": [ "application/json" ], @@ -36,12 +41,13 @@ const docTemplate = `{ "tags": [ "about" ], - "summary": "About", + "summary": "Get information about the server", "responses": { "200": { "description": "OK", "schema": { - "type": "msg" + "type": "object", + "additionalProperties": true } } } @@ -240,6 +246,11 @@ const docTemplate = `{ }, "/workflow/create": { "post": { + "security": [ + { + "Bearer": [] + } + ], "description": "Create a new workflow", "consumes": [ "application/json" @@ -258,7 +269,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.Workflow" + "$ref": "#/definitions/models.WorkflowDTO" } } ], @@ -266,7 +277,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.Workflow" + "$ref": "#/definitions/models.WorkflowDTO" } }, "400": { @@ -283,6 +294,11 @@ const docTemplate = `{ }, "/workflow/delete/{id}": { "delete": { + "security": [ + { + "Bearer": [] + } + ], "description": "Delete a workflow by ID", "consumes": [ "application/json" @@ -345,6 +361,11 @@ const docTemplate = `{ }, "/workflow/list": { "get": { + "security": [ + { + "Bearer": [] + } + ], "description": "List all workflows", "consumes": [ "application/json" @@ -362,7 +383,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.Workflow" + "$ref": "#/definitions/models.WorkflowDTO" } } } @@ -371,6 +392,40 @@ const docTemplate = `{ } }, "definitions": { + "models.EventDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ParametersDTO" + } + }, + "service_id": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/models.EventType" + } + } + }, + "models.EventType": { + "type": "string", + "enum": [ + "action", + "reaction" + ], + "x-enum-varnames": [ + "ActionEventType", + "ReactionEventType" + ] + }, "models.LoginRequest": { "type": "object", "required": [ @@ -386,6 +441,23 @@ const docTemplate = `{ } } }, + "models.ParametersDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "event_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, "models.RegisterRequest": { "type": "object", "required": [ @@ -405,8 +477,51 @@ const docTemplate = `{ } } }, - "models.Workflow": { - "type": "object" + "models.WorkflowDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/models.EventDTO" + } + }, + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/models.WorkflowStatus" + }, + "user_id": { + "type": "integer" + } + } + }, + "models.WorkflowStatus": { + "type": "string", + "enum": [ + "pending", + "processed", + "failed" + ], + "x-enum-varnames": [ + "WorkflowStatusPending", + "WorkflowStatusProcessed", + "WorkflowStatusFailed" + ] + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" } } }` diff --git a/server/docs/swagger.json b/server/docs/swagger.json index 6befab3..647141d 100644 --- a/server/docs/swagger.json +++ b/server/docs/swagger.json @@ -20,7 +20,12 @@ "paths": { "/about.json": { "get": { - "description": "about", + "security": [ + { + "Bearer": [] + } + ], + "description": "Get information about the server", "consumes": [ "application/json" ], @@ -30,12 +35,13 @@ "tags": [ "about" ], - "summary": "About", + "summary": "Get information about the server", "responses": { "200": { "description": "OK", "schema": { - "type": "msg" + "type": "object", + "additionalProperties": true } } } @@ -234,6 +240,11 @@ }, "/workflow/create": { "post": { + "security": [ + { + "Bearer": [] + } + ], "description": "Create a new workflow", "consumes": [ "application/json" @@ -252,7 +263,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.Workflow" + "$ref": "#/definitions/models.WorkflowDTO" } } ], @@ -260,7 +271,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.Workflow" + "$ref": "#/definitions/models.WorkflowDTO" } }, "400": { @@ -277,6 +288,11 @@ }, "/workflow/delete/{id}": { "delete": { + "security": [ + { + "Bearer": [] + } + ], "description": "Delete a workflow by ID", "consumes": [ "application/json" @@ -339,6 +355,11 @@ }, "/workflow/list": { "get": { + "security": [ + { + "Bearer": [] + } + ], "description": "List all workflows", "consumes": [ "application/json" @@ -356,7 +377,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.Workflow" + "$ref": "#/definitions/models.WorkflowDTO" } } } @@ -365,6 +386,40 @@ } }, "definitions": { + "models.EventDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ParametersDTO" + } + }, + "service_id": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/models.EventType" + } + } + }, + "models.EventType": { + "type": "string", + "enum": [ + "action", + "reaction" + ], + "x-enum-varnames": [ + "ActionEventType", + "ReactionEventType" + ] + }, "models.LoginRequest": { "type": "object", "required": [ @@ -380,6 +435,23 @@ } } }, + "models.ParametersDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "event_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, "models.RegisterRequest": { "type": "object", "required": [ @@ -399,8 +471,51 @@ } } }, - "models.Workflow": { - "type": "object" + "models.WorkflowDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/models.EventDTO" + } + }, + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/models.WorkflowStatus" + }, + "user_id": { + "type": "integer" + } + } + }, + "models.WorkflowStatus": { + "type": "string", + "enum": [ + "pending", + "processed", + "failed" + ], + "x-enum-varnames": [ + "WorkflowStatusPending", + "WorkflowStatusProcessed", + "WorkflowStatusFailed" + ] + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" } } } \ No newline at end of file diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml index 4c7f76c..d091bc0 100644 --- a/server/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -1,5 +1,28 @@ basePath: / definitions: + models.EventDTO: + properties: + description: + type: string + name: + type: string + parameters: + items: + $ref: '#/definitions/models.ParametersDTO' + type: array + service_id: + type: integer + type: + $ref: '#/definitions/models.EventType' + type: object + models.EventType: + enum: + - action + - reaction + type: string + x-enum-varnames: + - ActionEventType + - ReactionEventType models.LoginRequest: properties: email: @@ -10,6 +33,17 @@ definitions: - email - password type: object + models.ParametersDTO: + properties: + description: + type: string + event_id: + type: integer + name: + type: string + type: + type: string + type: object models.RegisterRequest: properties: email: @@ -23,8 +57,33 @@ definitions: - password - username type: object - models.Workflow: + models.WorkflowDTO: + properties: + description: + type: string + events: + items: + $ref: '#/definitions/models.EventDTO' + type: array + is_active: + type: boolean + name: + type: string + status: + $ref: '#/definitions/models.WorkflowStatus' + user_id: + type: integer type: object + models.WorkflowStatus: + enum: + - pending + - processed + - failed + type: string + x-enum-varnames: + - WorkflowStatusPending + - WorkflowStatusProcessed + - WorkflowStatusFailed host: localhost:8080 info: contact: @@ -43,15 +102,18 @@ paths: get: consumes: - application/json - description: about + description: Get information about the server produces: - application/json responses: "200": description: OK schema: - type: msg - summary: About + additionalProperties: true + type: object + security: + - Bearer: [] + summary: Get information about the server tags: - about /auth/health: @@ -190,20 +252,22 @@ paths: name: workflow required: true schema: - $ref: '#/definitions/models.Workflow' + $ref: '#/definitions/models.WorkflowDTO' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/models.Workflow' + $ref: '#/definitions/models.WorkflowDTO' "400": description: Bad Request schema: additionalProperties: type: string type: object + security: + - Bearer: [] summary: Create a workflow tags: - workflow @@ -245,6 +309,8 @@ paths: additionalProperties: type: string type: object + security: + - Bearer: [] summary: Delete a workflow tags: - workflow @@ -260,9 +326,16 @@ paths: description: OK schema: items: - $ref: '#/definitions/models.Workflow' + $ref: '#/definitions/models.WorkflowDTO' type: array + security: + - Bearer: [] summary: List workflows tags: - workflow +securityDefinitions: + BearerAuth: + in: header + name: Authorization + type: apiKey swagger: "2.0" diff --git a/server/internal/controllers/about.go b/server/internal/controllers/about.go index 00b33bc..dd8043c 100644 --- a/server/internal/controllers/about.go +++ b/server/internal/controllers/about.go @@ -10,24 +10,87 @@ import ( "time" ) -func getServices() (services []models.Service) { - value := pkg.DB.Where("id > 0").Find(&services) - if value.Error != nil { - log.Printf("Error loading services from db: %v", value.Error) - return +func getServiceFromType(serviceType string, service models.Service) models.ServiceList { + if err := pkg.DB.Preload("Events", "type = ?", serviceType). + Where("id = ?", service.ID). + First(&service).Error; err != nil { + log.Error().Err(err).Msg("Failed to load service with events") + return models.ServiceList{} } - return services + var actions []models.Action + var reactions []models.Reaction + for _, event := range service.Events { + pkg.DB.Preload("Parameters").Find(&event) + var parameters []models.Parameter + for _, param := range event.Parameters { + parameters = append(parameters, models.Parameter{ + Name: param.Name, + Description: param.Description, + Type: param.Type, + }) + } + + if serviceType == "action" { + actions = append(actions, models.Action{ + Name: event.Name, + Description: event.Description, + Parameters: parameters, + }) + } else if serviceType == "reaction" { + reactions = append(reactions, models.Reaction{ + Name: event.Name, + Description: event.Description, + Parameters: parameters, + }) + } + } + + return models.ServiceList{ + Name: service.Name, + Actions: actions, + Reaction: reactions, + } +} + +func getServices() []models.ServiceList { + var services []models.Service + var serviceList []models.ServiceList + + if err := pkg.DB.Preload("Events").Find(&services).Error; err != nil { + log.Error().Err(err).Msg("Failed to load services") + return nil + } + + for _, service := range services { + actions := getServiceFromType("action", service) + reactions := getServiceFromType("reaction", service) + serviceList = append(serviceList, models.ServiceList{ + Name: service.Name, + Actions: actions.Actions, + Reaction: reactions.Reaction, + }) + } + + return serviceList } -// About handler with cached service data +// About godoc +// @Summary Get information about the server +// @Description Get information about the server +// @Tags about +// @Accept json +// @Security Bearer +// @Produce json +// @Success 200 {object} map[string]interface{} +// @Router /about.json [get] func About(c *gin.Context) { var msg struct { Client struct { Host string `json:"host"` } `json:"client"` Server struct { - CurrentTime string `json:"current_time"` - Services []models.Service `json:"services"` + CurrentTime string `json:"current_time"` + Services []models.ServiceList `json:"services"` } `json:"server"` } diff --git a/server/internal/controllers/workflow.go b/server/internal/controllers/workflow.go index fe6ef79..e12ab8e 100644 --- a/server/internal/controllers/workflow.go +++ b/server/internal/controllers/workflow.go @@ -11,11 +11,12 @@ import ( // WorkflowCreate godoc // @Summary Create a workflow // @Description Create a new workflow +// @Security Bearer // @Tags workflow // @Accept json // @Produce json -// @Param workflow body models.Workflow true "workflow" -// @Success 200 {object} models.Workflow +// @Param workflow body models.WorkflowDTO true "workflow" +// @Success 200 {object} models.WorkflowDTO // @Failure 400 {object} map[string]string // @Router /workflow/create [post] func WorkflowCreate(c *gin.Context) { @@ -36,10 +37,11 @@ func WorkflowCreate(c *gin.Context) { // WorkflowList godoc // @Summary List workflows // @Description List all workflows +// @Security Bearer // @Tags workflow // @Accept json // @Produce json -// @Success 200 {object} []models.Workflow +// @Success 200 {object} []models.WorkflowDTO // @Router /workflow/list [get] func WorkflowList(c *gin.Context) { var workflows []models.Workflow @@ -54,6 +56,7 @@ func WorkflowList(c *gin.Context) { // WorkflowDelete godoc // @Summary Delete a workflow // @Description Delete a workflow by ID +// @Security Bearer // @Tags workflow // @Accept json // @Produce json diff --git a/server/internal/models/about.go b/server/internal/models/about.go index c3a91b9..f5795fa 100644 --- a/server/internal/models/about.go +++ b/server/internal/models/about.go @@ -1,25 +1,25 @@ package models -type parameter struct { +type Parameter struct { Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` } -type action struct { +type Action struct { Name string `json:"name"` Description string `json:"description"` - Parameters []parameter `json:"parameters"` + Parameters []Parameter `json:"parameters"` } -type reaction struct { +type Reaction struct { Name string `json:"name"` Description string `json:"description"` - Parameters []parameter `json:"parameters"` + Parameters []Parameter `json:"parameters"` } type ServiceList struct { Name string `json:"name"` - Actions []action `json:"actions"` - Reaction []reaction `json:"reactions"` + Actions []Action `json:"actions"` + Reaction []Reaction `json:"reactions"` } diff --git a/server/internal/models/apiDTO.go b/server/internal/models/apiDTO.go new file mode 100644 index 0000000..07821ca --- /dev/null +++ b/server/internal/models/apiDTO.go @@ -0,0 +1,25 @@ +package models + +type WorkflowDTO struct { + UserID uint `json:"user_id"` + Name string `json:"name"` + Description string `json:"description"` + Status WorkflowStatus `json:"status"` + IsActive bool `json:"is_active"` + Events []EventDTO `json:"events"` +} + +type EventDTO struct { + Name string `json:"name"` + Description string `json:"description"` + ServiceID uint `gorm:"foreignKey:ServiceID" json:"service_id"` + Parameters []ParametersDTO `json:"parameters"` + Type EventType `gorm:"type:enum('action', 'reaction');not null" json:"type"` +} + +type ParametersDTO struct { + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + EventID uint `gorm:"foreignKey:EventID" json:"event_id"` +} diff --git a/server/internal/models/service.go b/server/internal/models/service.go index 0272b26..e0c3ed7 100644 --- a/server/internal/models/service.go +++ b/server/internal/models/service.go @@ -5,5 +5,5 @@ import "gorm.io/gorm" type Service struct { gorm.Model Name string `json:"name"` - Events []Event `json:"events"` + Events []Event `gorm:"constraint:OnDelete:CASCADE;" json:"events"` } diff --git a/server/internal/pkg/about.go b/server/internal/pkg/about.go index dadf8fd..fc32461 100644 --- a/server/internal/pkg/about.go +++ b/server/internal/pkg/about.go @@ -3,7 +3,9 @@ package pkg import ( "AREA/internal/consts" "AREA/internal/models" + "errors" "github.com/goccy/go-json" + "gorm.io/gorm" "io/ioutil" "log" "path/filepath" @@ -23,23 +25,90 @@ func InitServiceList() { log.Printf("Error reading file %s: %v", file, err) continue } - var srv models.Service err = json.Unmarshal(data, &srv) if err != nil { log.Printf("Error unmarshalling file %s: %v", file, err) continue } - //DB.Create(&srv) - + if err := processService(&srv); err != nil { + log.Printf("Error processing service '%s': %v", srv.Name, err) + continue + } services = append(services, srv) } - //create or update services - value := DB.Clauses(models.Service{}).Create(services) - if value.Error != nil { - log.Printf("Error saving services in db: %v", value.Error) - return - } + log.Printf("Loaded %d services at startup in db.", len(services)) +} + +func processService(srv *models.Service) error { + var existingService models.Service + err := DB.Where("name = ?", srv.Name).First(&existingService).Error + if err == nil { + return updateService(srv, &existingService) + } else if errors.Is(err, gorm.ErrRecordNotFound) { + if err := DB.Create(srv).Error; err != nil { + return err + } + log.Printf("Created new service '%s' in the database.", srv.Name) + return nil + } + return err +} + +func updateService(newService *models.Service, existingService *models.Service) error { + for _, newEvent := range newService.Events { + if err := processEvent(newEvent, existingService.ID); err != nil { + log.Printf("Error processing event '%s': %v", newEvent.Name, err) + } + } + + log.Printf("Updated existing service '%s' in the database.", existingService.Name) + return DB.Save(existingService).Error +} + +func processEvent(newEvent models.Event, serviceID uint) error { + var existingEvent models.Event + + err := DB.Where("name = ? AND service_id = ?", newEvent.Name, serviceID).First(&existingEvent).Error + if err == nil { + return updateEvent(newEvent, &existingEvent) + } else if errors.Is(err, gorm.ErrRecordNotFound) { + newEvent.ServiceID = serviceID + if err := DB.Create(&newEvent).Error; err != nil { + return err + } + log.Printf("Created new event '%s' in the database.", newEvent.Name) + return nil + } + return err +} +func updateEvent(newEvent models.Event, existingEvent *models.Event) error { + for _, newParam := range newEvent.Parameters { + if err := processParameter(newParam, existingEvent.ID); err != nil { + log.Printf("Error processing parameter '%s': %v", newParam.Name, err) + } + } + existingEvent.Description = newEvent.Description + existingEvent.Type = newEvent.Type + return DB.Save(existingEvent).Error +} + +func processParameter(newParam models.Parameters, eventID uint) error { + var existingParam models.Parameters + err := DB.Where("name = ? AND event_id = ?", newParam.Name, eventID).First(&existingParam).Error + if err == nil { + existingParam.Description = newParam.Description + existingParam.Type = newParam.Type + return DB.Save(&existingParam).Error + } else if errors.Is(err, gorm.ErrRecordNotFound) { + newParam.EventID = eventID + if err := DB.Create(&newParam).Error; err != nil { + return err + } + log.Printf("Created new parameter '%s' in the database.", newParam.Name) + return nil + } + return err } diff --git a/server/internal/pkg/db.go b/server/internal/pkg/db.go index 47f6b1e..56ec36e 100644 --- a/server/internal/pkg/db.go +++ b/server/internal/pkg/db.go @@ -15,9 +15,9 @@ func migrateDB() error { err := DB.AutoMigrate( &models.User{}, &models.Workflow{}, + &models.Service{}, &models.Event{}, &models.Parameters{}, - &models.Service{}, ) if err != nil { log.Fatalf("Failed to migrate DB: %v", err) diff --git a/server/main.go b/server/main.go index 3c1620b..55dc9db 100644 --- a/server/main.go +++ b/server/main.go @@ -24,6 +24,10 @@ import ( // @host localhost:8080 // @BasePath / + +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization func main() { config.LoadConfig() pkg.InitDB()