diff --git a/cmd/laas/docs/docs.go b/cmd/laas/docs/docs.go index 5370cfb8..ae9c30f3 100644 --- a/cmd/laas/docs/docs.go +++ b/cmd/laas/docs/docs.go @@ -2475,6 +2475,9 @@ const docTemplate = `{ "datatypes.JSONType-models_LicenseDBSchemaExtension": { "type": "object" }, + "datatypes.JSONType-models_ObligationSchemaExtension": { + "type": "object" + }, "models.APICollection": { "type": "object", "properties": { @@ -2825,7 +2828,15 @@ const docTemplate = `{ } }, "models.LicenseDBSchemaExtension": { - "type": "object" + "type": "object", + "properties": { + "license_explanation": { + "type": "string" + }, + "license_suffix": { + "type": "string" + } + } }, "models.LicenseError": { "type": "object", @@ -3089,6 +3100,9 @@ const docTemplate = `{ "comment": { "type": "string" }, + "externalRef": { + "$ref": "#/definitions/datatypes.JSONType-models_ObligationSchemaExtension" + }, "id": { "type": "integer" }, @@ -3184,6 +3198,9 @@ const docTemplate = `{ "comment": { "type": "string" }, + "external_ref": { + "$ref": "#/definitions/models.ObligationSchemaExtension" + }, "modifications": { "type": "boolean", "example": true @@ -3324,6 +3341,17 @@ const docTemplate = `{ } } }, + "models.ObligationSchemaExtension": { + "type": "object", + "properties": { + "obligation_explanation": { + "type": "string" + }, + "obligation_suffix": { + "type": "string" + } + } + }, "models.ObligationType": { "type": "object", "required": [ @@ -3371,6 +3399,10 @@ const docTemplate = `{ "comment": { "type": "string" }, + "external_ref": { + "type": "object", + "additionalProperties": true + }, "modifications": { "type": "boolean", "example": true diff --git a/cmd/laas/docs/swagger.json b/cmd/laas/docs/swagger.json index 5833455e..5d01cfbb 100644 --- a/cmd/laas/docs/swagger.json +++ b/cmd/laas/docs/swagger.json @@ -2468,6 +2468,9 @@ "datatypes.JSONType-models_LicenseDBSchemaExtension": { "type": "object" }, + "datatypes.JSONType-models_ObligationSchemaExtension": { + "type": "object" + }, "models.APICollection": { "type": "object", "properties": { @@ -2818,7 +2821,15 @@ } }, "models.LicenseDBSchemaExtension": { - "type": "object" + "type": "object", + "properties": { + "license_explanation": { + "type": "string" + }, + "license_suffix": { + "type": "string" + } + } }, "models.LicenseError": { "type": "object", @@ -3082,6 +3093,9 @@ "comment": { "type": "string" }, + "externalRef": { + "$ref": "#/definitions/datatypes.JSONType-models_ObligationSchemaExtension" + }, "id": { "type": "integer" }, @@ -3177,6 +3191,9 @@ "comment": { "type": "string" }, + "external_ref": { + "$ref": "#/definitions/models.ObligationSchemaExtension" + }, "modifications": { "type": "boolean", "example": true @@ -3317,6 +3334,17 @@ } } }, + "models.ObligationSchemaExtension": { + "type": "object", + "properties": { + "obligation_explanation": { + "type": "string" + }, + "obligation_suffix": { + "type": "string" + } + } + }, "models.ObligationType": { "type": "object", "required": [ @@ -3364,6 +3392,10 @@ "comment": { "type": "string" }, + "external_ref": { + "type": "object", + "additionalProperties": true + }, "modifications": { "type": "boolean", "example": true diff --git a/cmd/laas/docs/swagger.yaml b/cmd/laas/docs/swagger.yaml index dc45c740..bdb858ed 100644 --- a/cmd/laas/docs/swagger.yaml +++ b/cmd/laas/docs/swagger.yaml @@ -2,6 +2,8 @@ basePath: /api/v1 definitions: datatypes.JSONType-models_LicenseDBSchemaExtension: type: object + datatypes.JSONType-models_ObligationSchemaExtension: + type: object models.APICollection: properties: authenticated: @@ -245,6 +247,11 @@ definitions: type: integer type: object models.LicenseDBSchemaExtension: + properties: + license_explanation: + type: string + license_suffix: + type: string type: object models.LicenseError: properties: @@ -431,6 +438,8 @@ definitions: $ref: '#/definitions/models.ObligationClassification' comment: type: string + externalRef: + $ref: '#/definitions/datatypes.JSONType-models_ObligationSchemaExtension' id: type: integer licenses: @@ -490,6 +499,8 @@ definitions: type: string comment: type: string + external_ref: + $ref: '#/definitions/models.ObligationSchemaExtension' modifications: example: true type: boolean @@ -595,6 +606,13 @@ definitions: example: 200 type: integer type: object + models.ObligationSchemaExtension: + properties: + obligation_explanation: + type: string + obligation_suffix: + type: string + type: object models.ObligationType: properties: type: @@ -627,6 +645,9 @@ definitions: type: string comment: type: string + external_ref: + additionalProperties: true + type: object modifications: example: true type: boolean diff --git a/cmd/laas/gen_external_ref_schema.go b/cmd/laas/gen_external_ref_schema.go index 571d746a..5a7dc116 100644 --- a/cmd/laas/gen_external_ref_schema.go +++ b/cmd/laas/gen_external_ref_schema.go @@ -36,15 +36,20 @@ type ExternalRefFields struct { Fields []ExternalRefFieldMetaData `yaml:"fields"` } +type ExternalRefYAML struct { + License ExternalRefFields `yaml:"license"` + Obligation ExternalRefFields `yaml:"obligation"` +} + func main() { - externalRefFields := ExternalRefFields{} + externalRefYAML := ExternalRefYAML{} fieldsMetadata, err := os.ReadFile(PATH_EXTERNAL_REF_CONFIG_FILE) if err != nil { log.Fatalf("Failed to instantiate json schema for external ref in license: %v", err) } - err = yaml.Unmarshal(fieldsMetadata, &externalRefFields) + err = yaml.Unmarshal(fieldsMetadata, &externalRefYAML) if err != nil { log.Fatalf("Failed to instantiate json schema for external ref in license: %v", err) } @@ -56,9 +61,34 @@ func main() { // REUSE-IgnoreStart - var fields []jen.Code + var licenseFields, obligationFields []jen.Code + + for _, f := range externalRefYAML.License.Fields { + field := jen.Id(f.StructFieldName).Op("*") + if f.StructFieldName == "" { + err = errors.New("field struct_field_name is missing in external_ref_fields.yaml") + } + switch f.Type { + case "boolean": + field = field.Bool() + case "string": + field = field.String() + case "int": + field = field.Int64() + default: + err = fmt.Errorf("type %s in external_ref_fields.yaml is not supported", f.Type) + } + if err != nil { + log.Fatalf("Failed to instantiate json schema for external ref in license: %v", err) + return + } + field = field.Tag(map[string]string{"json": fmt.Sprintf("%s,omitempty", f.Name)}) + licenseFields = append(licenseFields, field) + } + + f.Type().Id("LicenseDBSchemaExtension").Struct(licenseFields...) - for _, f := range externalRefFields.Fields { + for _, f := range externalRefYAML.Obligation.Fields { field := jen.Id(f.StructFieldName).Op("*") if f.StructFieldName == "" { err = errors.New("field struct_field_name is missing in external_ref_fields.yaml") @@ -77,11 +107,11 @@ func main() { log.Fatalf("Failed to instantiate json schema for external ref in license: %v", err) return } - field = field.Tag(map[string]string{"json": fmt.Sprintf("%s,omitempty", f.Name), "swaggerignore": "true"}) - fields = append(fields, field) + field = field.Tag(map[string]string{"json": fmt.Sprintf("%s,omitempty", f.Name)}) + obligationFields = append(obligationFields, field) } - f.Type().Id("LicenseDBSchemaExtension").Struct(fields...) + f.Type().Id("ObligationSchemaExtension").Struct(obligationFields...) f.Save(PATH_EXTERNAL_REF_STRUCT_FILE) } diff --git a/external_ref_fields.example.yaml b/external_ref_fields.example.yaml index 5d12c836..1c7262f7 100644 --- a/external_ref_fields.example.yaml +++ b/external_ref_fields.example.yaml @@ -1,16 +1,32 @@ # SPDX-License-Identifier: GPL-2.0-only # SPDX-FileCopyrightText: FOSSology contributors -fields: - - name: "license_suffix" - type: "string" - struct_field_name: "LicenseSuffix" - label: "License Suffix" - formComponentPath: "../components/dynamic/inputField" - componentType: "input" - - name: "license_explanation" - type: "string" - struct_field_name: "LicenseExplanation" - label: "License Explanation" - formComponentPath: "../components/dynamic/inputField" - componentType: "input" \ No newline at end of file +license: + fields: + - name: "license_suffix" + type: "string" + struct_field_name: "LicenseSuffix" + label: "License Suffix" + formComponentPath: "../components/dynamic/inputField" + componentType: "input" + - name: "license_explanation" + type: "string" + struct_field_name: "LicenseExplanation" + label: "License Explanation" + formComponentPath: "../components/dynamic/inputField" + componentType: "input" + +obligation: + fields: + - name: "obligation_suffix" + type: "string" + struct_field_name: "ObligationSuffix" + label: "Obligation Suffix" + formComponentPath: "../components/dynamic/inputField" + componentType: "input" + - name: "obligation_explanation" + type: "string" + struct_field_name: "ObligationExplanation" + label: "Obligation Explanation" + formComponentPath: "../components/dynamic/inputField" + componentType: "input" \ No newline at end of file diff --git a/pkg/api/obligations.go b/pkg/api/obligations.go index 49b87a7c..d7d8da52 100644 --- a/pkg/api/obligations.go +++ b/pkg/api/obligations.go @@ -15,6 +15,7 @@ import ( "fmt" "net/http" "path/filepath" + "reflect" "strconv" "strings" "time" @@ -280,7 +281,21 @@ func UpdateObligation(c *gin.Context) { if err := db.DB.Transaction(func(tx *gorm.DB) error { // https://gorm.io/docs/context.html#Context-in-Hooks-x2F-Callbacks ctx := context.WithValue(context.Background(), models.ContextKey("oldObligation"), &oldObligation) - if err := tx.WithContext(ctx).Omit("Licenses", "Topic").Updates(&newObligation).Error; err != nil { + + if err := tx.WithContext(ctx).Omit("Licenses", "Topic", "ExternalRef").Updates(&newObligation).Error; err != nil { + return err + } + + // Overwrite values of existing keys, add new key value pairs and remove keys with null values. + if err := tx.Debug().Model(&models.Obligation{}).Where(&models.Obligation{Id: newObligation.Id}).UpdateColumn("external_ref", gorm.Expr("jsonb_strip_nulls(COALESCE(external_ref, '{}'::jsonb) || ?)", updates.ExternalRef)).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update license", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) return err } @@ -683,6 +698,32 @@ func addChangelogsForObligation(tx *gorm.DB, userId int64, utils.AddChangelog("Text Updatable", oldObligation.TextUpdatable, newObligation.TextUpdatable, &changes) + oldLicenseExternalRef := oldObligation.ExternalRef.Data() + oldExternalRefVal := reflect.ValueOf(oldLicenseExternalRef) + typesOf := oldExternalRefVal.Type() + + newLicenseExternalRef := newObligation.ExternalRef.Data() + newExternalRefVal := reflect.ValueOf(newLicenseExternalRef) + + for i := 0; i < oldExternalRefVal.NumField(); i++ { + fieldName := typesOf.Field(i).Name + + switch typesOf.Field(i).Type.String() { + case "*boolean": + oldFieldPtr, _ := oldExternalRefVal.Field(i).Interface().(*bool) + newFieldPtr, _ := newExternalRefVal.Field(i).Interface().(*bool) + utils.AddChangelog(fmt.Sprintf("External Reference %s", fieldName), oldFieldPtr, newFieldPtr, &changes) + case "*string": + oldFieldPtr, _ := oldExternalRefVal.Field(i).Interface().(*string) + newFieldPtr, _ := newExternalRefVal.Field(i).Interface().(*string) + utils.AddChangelog(fmt.Sprintf("External Reference %s", fieldName), oldFieldPtr, newFieldPtr, &changes) + case "*int": + oldFieldPtr, _ := oldExternalRefVal.Field(i).Interface().(*int) + newFieldPtr, _ := newExternalRefVal.Field(i).Interface().(*int) + utils.AddChangelog(fmt.Sprintf("External Reference %s", fieldName), oldFieldPtr, newFieldPtr, &changes) + } + } + if len(changes) != 0 { audit := models.Audit{ UserId: userId, diff --git a/pkg/db/migrations/000012_obligations_externalref.down.sql b/pkg/db/migrations/000012_obligations_externalref.down.sql new file mode 100644 index 00000000..06ec0f1b --- /dev/null +++ b/pkg/db/migrations/000012_obligations_externalref.down.sql @@ -0,0 +1,5 @@ +-- SPDX-FileCopyrightText: 2025 Siemens AG +-- SPDX-FileCopyrightText: 2025 Dearsh Oberoi +-- SPDX-License-Identifier: GPL-2.0-only + +ALTER TABLE obligations DROP external_ref; diff --git a/pkg/db/migrations/000012_obligations_externalref.up.sql b/pkg/db/migrations/000012_obligations_externalref.up.sql new file mode 100644 index 00000000..c91aa921 --- /dev/null +++ b/pkg/db/migrations/000012_obligations_externalref.up.sql @@ -0,0 +1,5 @@ +-- SPDX-FileCopyrightText: 2025 Siemens AG +-- SPDX-FileCopyrightText: 2025 Dearsh Oberoi +-- SPDX-License-Identifier: GPL-2.0-only + +ALTER TABLE obligations ADD external_ref JSONB; \ No newline at end of file diff --git a/pkg/models/types.go b/pkg/models/types.go index 8e1838b2..d3455af4 100644 --- a/pkg/models/types.go +++ b/pkg/models/types.go @@ -19,6 +19,7 @@ import ( "github.com/fossology/LicenseDb/pkg/validations" "github.com/go-playground/validator/v10" + "gorm.io/datatypes" "gorm.io/gorm" ) @@ -251,20 +252,21 @@ type ObligationClassificationResponse struct { // Obligation represents an obligation record in the database. type Obligation struct { - Id int64 `gorm:"primary_key;column:id" ` - Topic *string `gorm:"column:topic"` - Text *string `gorm:"column:text"` - Modifications *bool `gorm:"column:modifications;default:false"` - Comment *string `gorm:"column:comment"` - Active *bool `gorm:"column:active;default:true"` - TextUpdatable *bool `gorm:"column:text_updatable;default:false" ` - Md5 string `gorm:"column:md5"` - ObligationClassificationId int64 `gorm:"column:obligation_classification_id"` - ObligationTypeId int64 `gorm:"column:obligation_type_id"` - Licenses []*LicenseDB `gorm:"many2many:obligation_licenses; joinForeignKey:obligation_id;joinReferences: license_db_id "` - Type *ObligationType `gorm:"foreignKey:ObligationTypeId; references:Id"` - Classification *ObligationClassification `gorm:"foreignKey:ObligationClassificationId ;references:Id"` - Category *string `json:"category" gorm:"default:GENERAL" enums:"DISTRIBUTION,PATENT,INTERNAL,CONTRACTUAL,EXPORT_CONTROL,GENERAL" example:"DISTRIBUTION"` + Id int64 `gorm:"primary_key;column:id" ` + Topic *string `gorm:"column:topic"` + Text *string `gorm:"column:text"` + Modifications *bool `gorm:"column:modifications;default:false"` + Comment *string `gorm:"column:comment"` + Active *bool `gorm:"column:active;default:true"` + TextUpdatable *bool `gorm:"column:text_updatable;default:false" ` + Md5 string `gorm:"column:md5"` + ObligationClassificationId int64 `gorm:"column:obligation_classification_id"` + ObligationTypeId int64 `gorm:"column:obligation_type_id"` + Licenses []*LicenseDB `gorm:"many2many:obligation_licenses; joinForeignKey:obligation_id;joinReferences: license_db_id "` + Type *ObligationType `gorm:"foreignKey:ObligationTypeId; references:Id"` + Classification *ObligationClassification `gorm:"foreignKey:ObligationClassificationId ;references:Id"` + Category *string `gorm:"default:GENERAL" enums:"DISTRIBUTION,PATENT,INTERNAL,CONTRACTUAL,EXPORT_CONTROL,GENERAL" example:"DISTRIBUTION"` + ExternalRef datatypes.JSONType[ObligationSchemaExtension] `gorm:"column:external_ref"` } func (Obligation) TableName() string { @@ -432,6 +434,7 @@ func (o *Obligation) BeforeUpdate(tx *gorm.DB) (err error) { // Custom json marshaller and unmarshaller for Obligation func (o *Obligation) MarshalJSON() ([]byte, error) { + externalRef := o.ExternalRef.Data() ob := ObligationDTO{ Topic: o.Topic, Text: o.Text, @@ -441,6 +444,7 @@ func (o *Obligation) MarshalJSON() ([]byte, error) { TextUpdatable: o.TextUpdatable, Shortnames: []string{}, Category: o.Category, + ExternalRef: &externalRef, } if o.Type != nil { @@ -503,34 +507,40 @@ func (o *Obligation) UnmarshalJSON(data []byte) error { }) } + if dto.ExternalRef != nil { + o.ExternalRef = datatypes.NewJSONType(*dto.ExternalRef) + } + return nil } // ObligationDTO represents an obligation json object. type ObligationDTO struct { - Topic *string `json:"topic" example:"copyleft" validate:"required"` - Type *string `json:"type" example:"RISK" validate:"required"` - Text *string `json:"text" example:"Source code be made available when distributing the software." validate:"required"` - Classification *string `json:"classification" example:"GREEN" validate:"required"` - Modifications *bool `json:"modifications" example:"true"` - Comment *string `json:"comment"` - Active *bool `json:"active"` - TextUpdatable *bool `json:"text_updatable" example:"true"` - Shortnames []string `json:"shortnames" validate:"required" example:"GPL-2.0-only,GPL-2.0-or-later"` - Category *string `json:"category" example:"DISTRIBUTION" validate:"required"` + Topic *string `json:"topic" example:"copyleft" validate:"required"` + Type *string `json:"type" example:"RISK" validate:"required"` + Text *string `json:"text" example:"Source code be made available when distributing the software." validate:"required"` + Classification *string `json:"classification" example:"GREEN" validate:"required"` + Modifications *bool `json:"modifications" example:"true"` + Comment *string `json:"comment"` + Active *bool `json:"active"` + TextUpdatable *bool `json:"text_updatable" example:"true"` + Shortnames []string `json:"shortnames" validate:"required" example:"GPL-2.0-only,GPL-2.0-or-later"` + Category *string `json:"category" example:"DISTRIBUTION" validate:"required"` + ExternalRef *ObligationSchemaExtension `json:"external_ref"` } // ObligationUpdateDTO represents an obligation json object. type ObligationUpdateDTO struct { - Topic *string `json:"-" example:"copyleft"` - Type *string `json:"type" example:"RISK"` - Text *string `json:"text" example:"Source code be made available when distributing the software."` - Classification *string `json:"classification" example:"GREEN"` - Modifications *bool `json:"modifications" example:"true"` - Comment *string `json:"comment"` - Active *bool `json:"active"` - TextUpdatable *bool `json:"text_updatable" example:"true"` - Category *string `json:"category" example:"DISTRIBUTION"` + Topic *string `json:"-" example:"copyleft"` + Type *string `json:"type" example:"RISK"` + Text *string `json:"text" example:"Source code be made available when distributing the software."` + Classification *string `json:"classification" example:"GREEN"` + Modifications *bool `json:"modifications" example:"true"` + Comment *string `json:"comment"` + Active *bool `json:"active"` + TextUpdatable *bool `json:"text_updatable" example:"true"` + Category *string `json:"category" example:"DISTRIBUTION"` + ExternalRef map[string]interface{} `json:"external_ref"` } func (obDto *ObligationUpdateDTO) Converter() *Obligation {