From a2daf914a58584a18e12905fc842512e32a06f5e Mon Sep 17 00:00:00 2001 From: deo002 Date: Tue, 15 Oct 2024 13:25:04 +0530 Subject: [PATCH] refactor(obligations): Make separate database tables for classification and type --- cmd/laas/docs/docs.go | 325 ++++++++----------- cmd/laas/docs/swagger.json | 325 ++++++++----------- cmd/laas/docs/swagger.yaml | 238 ++++++-------- cmd/laas/main.go | 25 +- pkg/api/audit.go | 44 +-- pkg/api/licenses.go | 8 +- pkg/api/obligationmap.go | 386 +++++----------------- pkg/api/obligations.go | 521 ++++++++++++------------------ pkg/db/db.go | 35 -- pkg/models/optional_data_types.go | 60 ---- pkg/models/types.go | 330 +++++++++++++++---- pkg/utils/util.go | 189 +++++++++++ 12 files changed, 1155 insertions(+), 1331 deletions(-) delete mode 100644 pkg/models/optional_data_types.go diff --git a/cmd/laas/docs/docs.go b/cmd/laas/docs/docs.go index c94903b..c3d2aee 100644 --- a/cmd/laas/docs/docs.go +++ b/cmd/laas/docs/docs.go @@ -1062,7 +1062,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.ObligationResponse" + "$ref": "#/definitions/models.SwaggerObligationResponse" } }, "404": { @@ -1098,7 +1098,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.ObligationPOSTRequestJSONSchema" + "$ref": "#/definitions/models.ObligationDTO" } } ], @@ -1106,7 +1106,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/models.ObligationResponse" + "$ref": "#/definitions/models.SwaggerObligationResponse" } }, "400": { @@ -1153,7 +1153,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.ObligationJSONFileFormat" + "$ref": "#/definitions/models.ObligationDTO" } } }, @@ -1303,7 +1303,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.ObligationResponse" + "$ref": "#/definitions/models.SwaggerObligationResponse" } }, "404": { @@ -1385,7 +1385,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.ObligationPATCHRequestJSONSchema" + "$ref": "#/definitions/models.ObligationUpdateDTO" } } ], @@ -1393,7 +1393,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.ObligationResponse" + "$ref": "#/definitions/models.SwaggerObligationResponse" } }, "400": { @@ -1894,6 +1894,12 @@ const docTemplate = `{ "type": "string", "example": "This license has been superseded." }, + "obligations": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Obligation" + } + }, "risk": { "type": "integer", "maximum": 5, @@ -2107,6 +2113,12 @@ const docTemplate = `{ "type": "string", "example": "This license has been superseded." }, + "obligations": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Obligation" + } + }, "risk": { "type": "integer", "maximum": 5, @@ -2142,85 +2154,66 @@ const docTemplate = `{ "type": "boolean" }, "classification": { - "type": "string", - "enum": [ - "green", - "white", - "yellow", - "red" - ], - "example": "green" + "$ref": "#/definitions/models.ObligationClassification" }, "comment": { "type": "string" }, "id": { - "type": "integer", - "example": 147 + "type": "integer" + }, + "licenses": { + "type": "array", + "items": { + "$ref": "#/definitions/models.LicenseDB" + } + }, + "md5": { + "type": "string" }, "modifications": { - "type": "boolean", - "example": true + "type": "boolean" + }, + "obligationClassificationId": { + "type": "integer" + }, + "obligationTypeId": { + "type": "integer" }, "text": { - "type": "string", - "example": "Source code be made available when distributing the software." + "type": "string" }, - "text_updatable": { - "type": "boolean", - "example": true + "textUpdatable": { + "type": "boolean" }, "topic": { - "type": "string", - "example": "copyleft" + "type": "string" }, "type": { - "type": "string", - "enum": [ - "obligation", - "restriction", - "risk", - "right" - ], - "example": "risk" + "$ref": "#/definitions/models.ObligationType" } } }, - "models.ObligationId": { + "models.ObligationClassification": { "type": "object", "properties": { - "id": { - "type": "integer", - "example": 31 + "classification": { + "type": "string" }, - "topic": { - "type": "string", - "example": "copyleft" - } - } - }, - "models.ObligationImportStatus": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.ObligationId" + "color": { + "type": "string" }, - "status": { - "type": "integer", - "example": 200 + "id": { + "type": "integer" } } }, - "models.ObligationJSONFileFormat": { + "models.ObligationDTO": { "type": "object", "required": [ - "active", "classification", - "comment", - "modifications", "shortnames", "text", - "text_updatable", "topic", "type" ], @@ -2230,19 +2223,14 @@ const docTemplate = `{ }, "classification": { "type": "string", - "enum": [ - "green", - "white", - "yellow", - "red" - ] + "example": "GREEN" }, "comment": { - "type": "string", - "example": "This is a comment." + "type": "string" }, "modifications": { - "type": "boolean" + "type": "boolean", + "example": true }, "shortnames": { "type": "array", @@ -2259,21 +2247,44 @@ const docTemplate = `{ "example": "Source code be made available when distributing the software." }, "text_updatable": { - "type": "boolean" + "type": "boolean", + "example": true }, "topic": { - "description": "binding:\"required\" tag cannot be used as is works only for request body", "type": "string", "example": "copyleft" }, "type": { "type": "string", - "enum": [ - "obligation", - "restriction", - "risk", - "right" - ] + "example": "RISK" + } + } + }, + "models.ObligationId": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 31 + }, + "topic": { + "type": "string", + "example": "copyleft" + } + } + }, + "models.ObligationImportStatus": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.ObligationId" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer", + "example": 200 } } }, @@ -2324,108 +2335,6 @@ const docTemplate = `{ } } }, - "models.ObligationPATCHRequestJSONSchema": { - "type": "object", - "properties": { - "active": { - "type": "boolean", - "example": true - }, - "classification": { - "type": "string", - "enum": [ - "green", - "white", - "yellow", - "red" - ] - }, - "comment": { - "type": "string", - "example": "This is a comment." - }, - "modifications": { - "type": "boolean" - }, - "text": { - "type": "string", - "example": "Source code be made available when distributing the software." - }, - "text_updatable": { - "type": "boolean" - }, - "type": { - "type": "string", - "enum": [ - "obligation", - "restriction", - "risk", - "right" - ] - } - } - }, - "models.ObligationPOSTRequestJSONSchema": { - "type": "object", - "required": [ - "active", - "classification", - "comment", - "modifications", - "shortnames", - "text", - "topic", - "type" - ], - "properties": { - "active": { - "type": "boolean", - "example": true - }, - "classification": { - "type": "string", - "enum": [ - "green", - "white", - "yellow", - "red" - ] - }, - "comment": { - "type": "string" - }, - "modifications": { - "type": "boolean" - }, - "shortnames": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "GPL-2.0-only", - "GPL-2.0-or-later" - ] - }, - "text": { - "type": "string", - "example": "Source code be made available when distributing the software." - }, - "topic": { - "type": "string", - "example": "copyleft" - }, - "type": { - "type": "string", - "enum": [ - "obligation", - "restriction", - "risk", - "right" - ] - } - } - }, "models.ObligationPreview": { "type": "object", "properties": { @@ -2459,21 +2368,45 @@ const docTemplate = `{ } } }, - "models.ObligationResponse": { + "models.ObligationType": { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Obligation" - } + "id": { + "type": "integer" }, - "paginationmeta": { - "$ref": "#/definitions/models.PaginationMeta" + "type": { + "type": "string" + } + } + }, + "models.ObligationUpdateDTO": { + "type": "object", + "properties": { + "active": { + "type": "boolean" }, - "status": { - "type": "integer", - "example": 200 + "classification": { + "type": "string", + "example": "GREEN" + }, + "comment": { + "type": "string" + }, + "modifications": { + "type": "boolean", + "example": true + }, + "text": { + "type": "string", + "example": "Source code be made available when distributing the software." + }, + "text_updatable": { + "type": "boolean", + "example": true + }, + "type": { + "type": "string", + "example": "RISK" } } }, @@ -2530,6 +2463,24 @@ const docTemplate = `{ } } }, + "models.SwaggerObligationResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ObligationDTO" + } + }, + "paginationmeta": { + "$ref": "#/definitions/models.PaginationMeta" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, "models.User": { "type": "object", "required": [ diff --git a/cmd/laas/docs/swagger.json b/cmd/laas/docs/swagger.json index e02fecb..3e251d7 100644 --- a/cmd/laas/docs/swagger.json +++ b/cmd/laas/docs/swagger.json @@ -1055,7 +1055,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.ObligationResponse" + "$ref": "#/definitions/models.SwaggerObligationResponse" } }, "404": { @@ -1091,7 +1091,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.ObligationPOSTRequestJSONSchema" + "$ref": "#/definitions/models.ObligationDTO" } } ], @@ -1099,7 +1099,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/models.ObligationResponse" + "$ref": "#/definitions/models.SwaggerObligationResponse" } }, "400": { @@ -1146,7 +1146,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.ObligationJSONFileFormat" + "$ref": "#/definitions/models.ObligationDTO" } } }, @@ -1296,7 +1296,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.ObligationResponse" + "$ref": "#/definitions/models.SwaggerObligationResponse" } }, "404": { @@ -1378,7 +1378,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.ObligationPATCHRequestJSONSchema" + "$ref": "#/definitions/models.ObligationUpdateDTO" } } ], @@ -1386,7 +1386,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.ObligationResponse" + "$ref": "#/definitions/models.SwaggerObligationResponse" } }, "400": { @@ -1887,6 +1887,12 @@ "type": "string", "example": "This license has been superseded." }, + "obligations": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Obligation" + } + }, "risk": { "type": "integer", "maximum": 5, @@ -2100,6 +2106,12 @@ "type": "string", "example": "This license has been superseded." }, + "obligations": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Obligation" + } + }, "risk": { "type": "integer", "maximum": 5, @@ -2135,85 +2147,66 @@ "type": "boolean" }, "classification": { - "type": "string", - "enum": [ - "green", - "white", - "yellow", - "red" - ], - "example": "green" + "$ref": "#/definitions/models.ObligationClassification" }, "comment": { "type": "string" }, "id": { - "type": "integer", - "example": 147 + "type": "integer" + }, + "licenses": { + "type": "array", + "items": { + "$ref": "#/definitions/models.LicenseDB" + } + }, + "md5": { + "type": "string" }, "modifications": { - "type": "boolean", - "example": true + "type": "boolean" + }, + "obligationClassificationId": { + "type": "integer" + }, + "obligationTypeId": { + "type": "integer" }, "text": { - "type": "string", - "example": "Source code be made available when distributing the software." + "type": "string" }, - "text_updatable": { - "type": "boolean", - "example": true + "textUpdatable": { + "type": "boolean" }, "topic": { - "type": "string", - "example": "copyleft" + "type": "string" }, "type": { - "type": "string", - "enum": [ - "obligation", - "restriction", - "risk", - "right" - ], - "example": "risk" + "$ref": "#/definitions/models.ObligationType" } } }, - "models.ObligationId": { + "models.ObligationClassification": { "type": "object", "properties": { - "id": { - "type": "integer", - "example": 31 + "classification": { + "type": "string" }, - "topic": { - "type": "string", - "example": "copyleft" - } - } - }, - "models.ObligationImportStatus": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.ObligationId" + "color": { + "type": "string" }, - "status": { - "type": "integer", - "example": 200 + "id": { + "type": "integer" } } }, - "models.ObligationJSONFileFormat": { + "models.ObligationDTO": { "type": "object", "required": [ - "active", "classification", - "comment", - "modifications", "shortnames", "text", - "text_updatable", "topic", "type" ], @@ -2223,19 +2216,14 @@ }, "classification": { "type": "string", - "enum": [ - "green", - "white", - "yellow", - "red" - ] + "example": "GREEN" }, "comment": { - "type": "string", - "example": "This is a comment." + "type": "string" }, "modifications": { - "type": "boolean" + "type": "boolean", + "example": true }, "shortnames": { "type": "array", @@ -2252,21 +2240,44 @@ "example": "Source code be made available when distributing the software." }, "text_updatable": { - "type": "boolean" + "type": "boolean", + "example": true }, "topic": { - "description": "binding:\"required\" tag cannot be used as is works only for request body", "type": "string", "example": "copyleft" }, "type": { "type": "string", - "enum": [ - "obligation", - "restriction", - "risk", - "right" - ] + "example": "RISK" + } + } + }, + "models.ObligationId": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 31 + }, + "topic": { + "type": "string", + "example": "copyleft" + } + } + }, + "models.ObligationImportStatus": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.ObligationId" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer", + "example": 200 } } }, @@ -2317,108 +2328,6 @@ } } }, - "models.ObligationPATCHRequestJSONSchema": { - "type": "object", - "properties": { - "active": { - "type": "boolean", - "example": true - }, - "classification": { - "type": "string", - "enum": [ - "green", - "white", - "yellow", - "red" - ] - }, - "comment": { - "type": "string", - "example": "This is a comment." - }, - "modifications": { - "type": "boolean" - }, - "text": { - "type": "string", - "example": "Source code be made available when distributing the software." - }, - "text_updatable": { - "type": "boolean" - }, - "type": { - "type": "string", - "enum": [ - "obligation", - "restriction", - "risk", - "right" - ] - } - } - }, - "models.ObligationPOSTRequestJSONSchema": { - "type": "object", - "required": [ - "active", - "classification", - "comment", - "modifications", - "shortnames", - "text", - "topic", - "type" - ], - "properties": { - "active": { - "type": "boolean", - "example": true - }, - "classification": { - "type": "string", - "enum": [ - "green", - "white", - "yellow", - "red" - ] - }, - "comment": { - "type": "string" - }, - "modifications": { - "type": "boolean" - }, - "shortnames": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "GPL-2.0-only", - "GPL-2.0-or-later" - ] - }, - "text": { - "type": "string", - "example": "Source code be made available when distributing the software." - }, - "topic": { - "type": "string", - "example": "copyleft" - }, - "type": { - "type": "string", - "enum": [ - "obligation", - "restriction", - "risk", - "right" - ] - } - } - }, "models.ObligationPreview": { "type": "object", "properties": { @@ -2452,21 +2361,45 @@ } } }, - "models.ObligationResponse": { + "models.ObligationType": { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Obligation" - } + "id": { + "type": "integer" }, - "paginationmeta": { - "$ref": "#/definitions/models.PaginationMeta" + "type": { + "type": "string" + } + } + }, + "models.ObligationUpdateDTO": { + "type": "object", + "properties": { + "active": { + "type": "boolean" }, - "status": { - "type": "integer", - "example": 200 + "classification": { + "type": "string", + "example": "GREEN" + }, + "comment": { + "type": "string" + }, + "modifications": { + "type": "boolean", + "example": true + }, + "text": { + "type": "string", + "example": "Source code be made available when distributing the software." + }, + "text_updatable": { + "type": "boolean", + "example": true + }, + "type": { + "type": "string", + "example": "RISK" } } }, @@ -2523,6 +2456,24 @@ } } }, + "models.SwaggerObligationResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ObligationDTO" + } + }, + "paginationmeta": { + "$ref": "#/definitions/models.PaginationMeta" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, "models.User": { "type": "object", "required": [ diff --git a/cmd/laas/docs/swagger.yaml b/cmd/laas/docs/swagger.yaml index 967cf06..38ae564 100644 --- a/cmd/laas/docs/swagger.yaml +++ b/cmd/laas/docs/swagger.yaml @@ -143,6 +143,10 @@ definitions: notes: example: This license has been superseded. type: string + obligations: + items: + $ref: '#/definitions/models.Obligation' + type: array risk: maximum: 5 minimum: 0 @@ -299,6 +303,10 @@ definitions: notes: example: This license has been superseded. type: string + obligations: + items: + $ref: '#/definitions/models.Obligation' + type: array risk: example: 1 maximum: 5 @@ -325,71 +333,52 @@ definitions: active: type: boolean classification: - enum: - - green - - white - - yellow - - red - example: green - type: string + $ref: '#/definitions/models.ObligationClassification' comment: type: string id: - example: 147 type: integer + licenses: + items: + $ref: '#/definitions/models.LicenseDB' + type: array + md5: + type: string modifications: - example: true type: boolean + obligationClassificationId: + type: integer + obligationTypeId: + type: integer text: - example: Source code be made available when distributing the software. type: string - text_updatable: - example: true + textUpdatable: type: boolean topic: - example: copyleft type: string type: - enum: - - obligation - - restriction - - risk - - right - example: risk - type: string + $ref: '#/definitions/models.ObligationType' type: object - models.ObligationId: + models.ObligationClassification: properties: - id: - example: 31 - type: integer - topic: - example: copyleft + classification: type: string - type: object - models.ObligationImportStatus: - properties: - data: - $ref: '#/definitions/models.ObligationId' - status: - example: 200 + color: + type: string + id: type: integer type: object - models.ObligationJSONFileFormat: + models.ObligationDTO: properties: active: type: boolean classification: - enum: - - green - - white - - yellow - - red + example: GREEN type: string comment: - example: This is a comment. type: string modifications: + example: true type: boolean shortnames: example: @@ -402,30 +391,40 @@ definitions: example: Source code be made available when distributing the software. type: string text_updatable: + example: true type: boolean topic: - description: binding:"required" tag cannot be used as is works only for request - body example: copyleft type: string type: - enum: - - obligation - - restriction - - risk - - right + example: RISK type: string required: - - active - classification - - comment - - modifications - shortnames - text - - text_updatable - topic - type type: object + models.ObligationId: + properties: + id: + example: 31 + type: integer + topic: + example: copyleft + type: string + type: object + models.ObligationImportStatus: + properties: + data: + $ref: '#/definitions/models.ObligationId' + message: + type: string + status: + example: 200 + type: integer + type: object models.ObligationMapResponse: properties: data: @@ -459,82 +458,6 @@ definitions: example: obligation type: string type: object - models.ObligationPATCHRequestJSONSchema: - properties: - active: - example: true - type: boolean - classification: - enum: - - green - - white - - yellow - - red - type: string - comment: - example: This is a comment. - type: string - modifications: - type: boolean - text: - example: Source code be made available when distributing the software. - type: string - text_updatable: - type: boolean - type: - enum: - - obligation - - restriction - - risk - - right - type: string - type: object - models.ObligationPOSTRequestJSONSchema: - properties: - active: - example: true - type: boolean - classification: - enum: - - green - - white - - yellow - - red - type: string - comment: - type: string - modifications: - type: boolean - shortnames: - example: - - GPL-2.0-only - - GPL-2.0-or-later - items: - type: string - type: array - text: - example: Source code be made available when distributing the software. - type: string - topic: - example: copyleft - type: string - type: - enum: - - obligation - - restriction - - risk - - right - type: string - required: - - active - - classification - - comment - - modifications - - shortnames - - text - - topic - - type - type: object models.ObligationPreview: properties: topic: @@ -558,17 +481,34 @@ definitions: example: 200 type: integer type: object - models.ObligationResponse: + models.ObligationType: properties: - data: - items: - $ref: '#/definitions/models.Obligation' - type: array - paginationmeta: - $ref: '#/definitions/models.PaginationMeta' - status: - example: 200 + id: type: integer + type: + type: string + type: object + models.ObligationUpdateDTO: + properties: + active: + type: boolean + classification: + example: GREEN + type: string + comment: + type: string + modifications: + example: true + type: boolean + text: + example: Source code be made available when distributing the software. + type: string + text_updatable: + example: true + type: boolean + type: + example: RISK + type: string type: object models.PaginationMeta: properties: @@ -608,6 +548,18 @@ definitions: - field - search_term type: object + models.SwaggerObligationResponse: + properties: + data: + items: + $ref: '#/definitions/models.ObligationDTO' + type: array + paginationmeta: + $ref: '#/definitions/models.PaginationMeta' + status: + example: 200 + type: integer + type: object models.User: properties: id: @@ -1349,7 +1301,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/models.ObligationResponse' + $ref: '#/definitions/models.SwaggerObligationResponse' "404": description: No obligations in DB schema: @@ -1371,14 +1323,14 @@ paths: name: obligation required: true schema: - $ref: '#/definitions/models.ObligationPOSTRequestJSONSchema' + $ref: '#/definitions/models.ObligationDTO' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/models.ObligationResponse' + $ref: '#/definitions/models.SwaggerObligationResponse' "400": description: Bad request body schema: @@ -1439,7 +1391,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/models.ObligationResponse' + $ref: '#/definitions/models.SwaggerObligationResponse' "404": description: No obligation with given topic found schema: @@ -1466,14 +1418,14 @@ paths: name: obligation required: true schema: - $ref: '#/definitions/models.ObligationPATCHRequestJSONSchema' + $ref: '#/definitions/models.ObligationUpdateDTO' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/models.ObligationResponse' + $ref: '#/definitions/models.SwaggerObligationResponse' "400": description: Invalid request schema: @@ -1543,7 +1495,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/models.ObligationJSONFileFormat' + $ref: '#/definitions/models.ObligationDTO' type: array "500": description: Failed to fetch obligations diff --git a/cmd/laas/main.go b/cmd/laas/main.go index 6382f4c..7080ed9 100644 --- a/cmd/laas/main.go +++ b/cmd/laas/main.go @@ -11,12 +11,14 @@ import ( "log" "github.com/joho/godotenv" + "gorm.io/gorm/clause" _ "github.com/dave/jennifer/jen" _ "github.com/fossology/LicenseDb/cmd/laas/docs" "github.com/fossology/LicenseDb/pkg/api" "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" + "github.com/fossology/LicenseDb/pkg/utils" ) // declare flags to input the basic requirement of database connection and the path of the data file @@ -68,12 +70,29 @@ func main() { log.Fatalf("Failed to automigrate database: %v", err) } - if err := db.DB.AutoMigrate(&models.ObligationMap{}); err != nil { - log.Fatalf("Failed to automigrate database: %v", err) + DEFAULT_OBLIGATION_TYPES := []*models.ObligationType{ + {Type: "OBLIGATION"}, + {Type: "RISK"}, + {Type: "RESTRICTION"}, + {Type: "RIGHT"}, + } + DEFAULT_OBLIGATION_CLASSIFICATIONS := []*models.ObligationClassification{ + {Classification: "GREEN", Color: "#00FF00"}, + {Classification: "WHITE", Color: "#FFFFFF"}, + {Classification: "YELLOW", Color: "#FFDE21"}, + {Classification: "RED", Color: "#FF0000"}, + } + + if err := db.DB.Clauses(clause.OnConflict{DoNothing: true}).Create(DEFAULT_OBLIGATION_TYPES).Error; err != nil { + log.Fatalf("Failed to seed database with default obligation types: %s", err.Error()) + } + + if err := db.DB.Clauses(clause.OnConflict{DoNothing: true}).Create(DEFAULT_OBLIGATION_CLASSIFICATIONS).Error; err != nil { + log.Fatalf("Failed to seed database with default obligation classifications: %s", err.Error()) } if *populatedb { - db.Populatedb(*datafile) + utils.Populatedb(*datafile) } r := api.Router() diff --git a/pkg/api/audit.go b/pkg/api/audit.go index 16ca34a..1978ab8 100644 --- a/pkg/api/audit.go +++ b/pkg/api/audit.go @@ -50,7 +50,15 @@ func GetAllAudit(c *gin.Context) { } for i := 0; i < len(audits); i++ { - if err := getAuditEntity(c, &audits[i]); err != nil { + if err := utils.GetAuditEntity(c, &audits[i]); err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "unable to find audits", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) return } } @@ -99,7 +107,7 @@ func GetAudit(c *gin.Context) { return } - if err := getAuditEntity(c, &audit); err != nil { + if err := utils.GetAuditEntity(c, &audit); err != nil { return } @@ -232,35 +240,3 @@ func GetChangeLogbyId(c *gin.Context) { } c.JSON(http.StatusOK, res) } - -// getAuditEntity is an utility function to fetch obligation or license associated with an audit -func getAuditEntity(c *gin.Context, audit *models.Audit) error { - if audit.Type == "license" || audit.Type == "License" { - audit.Entity = &models.LicenseDB{} - if err := db.DB.Where(&models.LicenseDB{Id: audit.TypeId}).First(&audit.Entity).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: "license corresponding with this audit does not exist", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return err - } - } else if audit.Type == "obligation" || audit.Type == "Obligation" { - audit.Entity = &models.Obligation{} - if err := db.DB.Where(&models.Obligation{Id: audit.TypeId}).First(&audit.Entity).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: "obligation corresponding with this audit does not exist", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return err - } - } - return nil -} diff --git a/pkg/api/licenses.go b/pkg/api/licenses.go index 7551fb7..8260528 100644 --- a/pkg/api/licenses.go +++ b/pkg/api/licenses.go @@ -442,7 +442,7 @@ func UpdateLicense(c *gin.Context) { newLicense := models.LicenseDB(updates) // Update all other fields except external_ref and rf_shortname - if err := tx.Model(&newLicense).Omit("external_ref", "rf_shortname").Clauses(clause.Returning{}).Where(models.LicenseDB{Id: oldLicense.Id}).Updates(newLicense).Error; err != nil { + if err := tx.Model(&newLicense).Omit("external_ref", "rf_shortname", "Obligations").Clauses(clause.Returning{}).Where(models.LicenseDB{Id: oldLicense.Id}).Updates(newLicense).Error; err != nil { er := models.LicenseError{ Status: http.StatusInternalServerError, Message: "Failed to update license", @@ -913,10 +913,14 @@ func ImportLicenses(c *gin.Context) { errMessage, importStatus, oldLicense, newLicense := utils.InsertOrUpdateLicenseOnImport(tx, &licenses[i], &externalRefs[i]) if importStatus == utils.IMPORT_FAILED { + erroredLicense := "" + if licenses[i].Shortname != nil { + erroredLicense = *licenses[i].Shortname + } res.Data = append(res.Data, models.LicenseError{ Status: http.StatusInternalServerError, Message: errMessage, - Error: *licenses[i].Shortname, + Error: erroredLicense, Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), }) diff --git a/pkg/api/obligationmap.go b/pkg/api/obligationmap.go index 6074ec1..009f492 100644 --- a/pkg/api/obligationmap.go +++ b/pkg/api/obligationmap.go @@ -1,23 +1,21 @@ // SPDX-FileCopyrightText: 2023 Siemens AG // SPDX-FileContributor: Gaurav Mishra +// SPDX-FileContributor: Dearsh Oberoi // // SPDX-License-Identifier: GPL-2.0-only package api import ( - "errors" "fmt" "net/http" - "strconv" - "strings" "time" "golang.org/x/exp/slices" - "gorm.io/gorm" "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" + "github.com/fossology/LicenseDb/pkg/utils" "github.com/gin-gonic/gin" ) @@ -37,13 +35,12 @@ import ( // @Router /obligation_maps/topic/{topic} [get] func GetObligationMapByTopic(c *gin.Context) { var obligation models.Obligation - var obMap []models.ObligationMap var resObMap models.ObligationMapUser var shortnameList []string topic := c.Param("topic") - if err := db.DB.Where(models.Obligation{Topic: topic}).First(&obligation).Error; err != nil { + if err := db.DB.Joins("Classification").Joins("Type").Preload("Licenses").Where(models.Obligation{Topic: &topic}).First(&obligation).Error; err != nil { er := models.LicenseError{ Status: http.StatusNotFound, Message: fmt.Sprintf("obligation with topic '%s' not found", topic), @@ -55,37 +52,13 @@ func GetObligationMapByTopic(c *gin.Context) { return } - if err := db.DB.Where(models.ObligationMap{ObligationPk: obligation.Id}).Find(&obMap).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: fmt.Sprintf("Obligation map not found for topic '%s'", topic), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return - } - - for i := 0; i < len(obMap); i++ { - var license models.LicenseDB - if err := db.DB.Where(models.LicenseDB{Id: obMap[i].RfPk}).First(&license).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: "Unable to fetch license shortnames", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return - } - shortnameList = append(shortnameList, *license.Shortname) + for _, lic := range obligation.Licenses { + shortnameList = append(shortnameList, *lic.Shortname) } resObMap = models.ObligationMapUser{ Topic: topic, - Type: obligation.Type, + Type: (*obligation.Type).Type, Shortnames: shortnameList, } @@ -115,12 +88,11 @@ func GetObligationMapByTopic(c *gin.Context) { // @Router /obligation_maps/license/{license} [get] func GetObligationMapByLicense(c *gin.Context) { var license models.LicenseDB - var obMap []models.ObligationMap var resObMapList []models.ObligationMapUser licenseShortName := c.Param("license") - if err := db.DB.Where(models.LicenseDB{Shortname: &licenseShortName}).First(&license).Error; err != nil { + if err := db.DB.Preload("Obligations").Preload("Obligations.Type").Preload("Obligations.Classification").Where(models.LicenseDB{Shortname: &licenseShortName}).First(&license).Error; err != nil { er := models.LicenseError{ Status: http.StatusNotFound, Message: fmt.Sprintf("license with shortname '%s' not found", licenseShortName), @@ -132,34 +104,10 @@ func GetObligationMapByLicense(c *gin.Context) { return } - if err := db.DB.Where(models.ObligationMap{RfPk: license.Id}).Find(&obMap).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: fmt.Sprintf("Obligation map not found for license '%s'", licenseShortName), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return - } - - for i := 0; i < len(obMap); i++ { - var obligation models.Obligation - if err := db.DB.Where(models.Obligation{Id: obMap[i].ObligationPk}).First(&obligation).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: fmt.Sprintf("Unable to fetch obligations linked with license '%s'", licenseShortName), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return - } + for _, ob := range license.Obligations { resObMapList = append(resObMapList, models.ObligationMapUser{ - Type: obligation.Type, - Topic: obligation.Topic, + Type: (*ob.Type).Type, + Topic: *ob.Topic, Shortnames: []string{licenseShortName}, }) } @@ -193,8 +141,7 @@ func GetObligationMapByLicense(c *gin.Context) { func PatchObligationMap(c *gin.Context) { var obligation models.Obligation var obMapInput models.LicenseMapShortnamesInput - var removeLicenseIds []int64 - var insertLicenseIds []int64 + var removeLicenses, insertLicenses []string topic := c.Param("topic") @@ -210,7 +157,7 @@ func PatchObligationMap(c *gin.Context) { return } - if err := db.DB.Where(models.Obligation{Topic: topic}).First(&obligation).Error; err != nil { + if err := db.DB.Preload("Licenses").Joins("Type").Where(models.Obligation{Topic: &topic}).First(&obligation).Error; err != nil { er := models.LicenseError{ Status: http.StatusNotFound, Message: fmt.Sprintf("obligation with topic '%s' not found", topic), @@ -222,56 +169,36 @@ func PatchObligationMap(c *gin.Context) { return } - for i := 0; i < len(obMapInput.MapInput); i++ { - var license models.LicenseDB - var obligationMap models.ObligationMap - if err := db.DB.Where(&models.LicenseDB{Shortname: &obMapInput.MapInput[i].Shortname}).First(&license).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: fmt.Sprintf("license with shortname '%s' not found", obMapInput.MapInput[i].Shortname), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return - } - if err := db.DB.Where(&models.ObligationMap{ObligationPk: obligation.Id, RfPk: license.Id}).First(&obligationMap).Error; err != nil { - // License not in map - if errors.Is(err, gorm.ErrRecordNotFound) { - if obMapInput.MapInput[i].Add { - // Add to insert slice - insertLicenseIds = append(insertLicenseIds, license.Id) + for _, lic := range obMapInput.MapInput { + if lic.Add { + found := false + for _, l := range obligation.Licenses { + if lic.Shortname == *l.Shortname { + found = true + break } - } else { - er := models.LicenseError{ - Status: http.StatusInternalServerError, - Message: fmt.Sprintf("unable to fetch obligation maps for obligation with topic '%s'", obligation.Topic), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusInternalServerError, er) - return } - - } else { - // License in map - if !obMapInput.MapInput[i].Add { - // Add to remove slice - removeLicenseIds = append(removeLicenseIds, license.Id) + if !found { + insertLicenses = append(insertLicenses, lic.Shortname) } + } else { + removeLicenses = append(removeLicenses, lic.Shortname) } } username := c.GetString("username") - - res, err := PerformObligationMapActions(username, obligation, removeLicenseIds, insertLicenseIds) - if err != nil { + newLicenseAssociations, errs := utils.PerformObligationMapActions(username, &obligation, removeLicenses, insertLicenses) + if len(errs) != 0 { + var combinedErrors string + for _, err := range errs { + if err != nil { + combinedErrors += fmt.Sprintf("%s\n", err) + } + } er := models.LicenseError{ Status: http.StatusInternalServerError, - Message: "Something went wrong", - Error: err.Error(), + Message: "Unable to add/remove some of the licenses", + Error: combinedErrors, Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } @@ -279,6 +206,20 @@ func PatchObligationMap(c *gin.Context) { return } + obMap := &models.ObligationMapUser{ + Topic: *obligation.Topic, + Type: (*obligation.Type).Type, + Shortnames: newLicenseAssociations, + } + + res := models.ObligationMapResponse{ + Data: []models.ObligationMapUser{*obMap}, + Status: http.StatusOK, + Meta: models.PaginationMeta{ + ResourceCount: 1, + }, + } + c.JSON(http.StatusOK, res) } @@ -300,22 +241,7 @@ func PatchObligationMap(c *gin.Context) { func UpdateLicenseInObligationMap(c *gin.Context) { var obligation models.Obligation var obMapInput models.LicenseShortnamesInput - var removeLicenseIds []int64 - var insertLicenseIds []int64 - - topic := c.Param("topic") - - if err := db.DB.Where(models.Obligation{Topic: topic}).First(&obligation).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: fmt.Sprintf("obligation with topic '%s' not found", topic), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return - } + var removeLicenses, insertLicenses []string if err := c.ShouldBindJSON(&obMapInput); err != nil { er := models.LicenseError{ @@ -329,146 +255,47 @@ func UpdateLicenseInObligationMap(c *gin.Context) { return } - obMapInput.Shortnames = slices.Compact(obMapInput.Shortnames) - - username := c.GetString("username") - - if err := GenerateDiffOfLicenses(c, &obligation, obMapInput.Shortnames, &removeLicenseIds, &insertLicenseIds); err != nil { - return - } - - res, err := PerformObligationMapActions(username, obligation, removeLicenseIds, insertLicenseIds) - if err != nil { - er := models.LicenseError{ - Status: http.StatusInternalServerError, - Message: "Something went wrong", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return - } - - c.JSON(http.StatusOK, res) -} - -// GenerateDiffOfLicenses calculates diff from the obligation maps list in database and the list provided by the user to determine the licenses to be -// inserted and the licenses to be removed. Basically, it replaces the list present in database by the list given by the user. -func GenerateDiffOfLicenses(c *gin.Context, obligation *models.Obligation, inputShortnames []string, removeLicenseIds, insertLicenseIds *[]int64) error { - var oldObMaps []models.ObligationMap - if err := db.DB.Where(models.ObligationMap{ObligationPk: obligation.Id}).Find(&oldObMaps).Error; err != nil { + topic := c.Param("topic") + if err := db.DB.Preload("Licenses").Joins("Type").Where(models.Obligation{Topic: &topic}).First(&obligation).Error; err != nil { er := models.LicenseError{ Status: http.StatusNotFound, - Message: fmt.Sprintf("obligation maps for obligation with topic '%s' not found", obligation.Topic), + Message: fmt.Sprintf("obligation with topic '%s' not found", topic), Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } c.JSON(http.StatusNotFound, er) - return err - } - - for i := 0; i < len(oldObMaps); i++ { - *removeLicenseIds = append(*removeLicenseIds, oldObMaps[i].RfPk) - } - - for i := 0; i < len(inputShortnames); i++ { - var license models.LicenseDB - var obligationMap models.ObligationMap - if err := db.DB.Where(&models.LicenseDB{Shortname: &inputShortnames[i]}).First(&license).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: fmt.Sprintf("license with shortname '%s' not found", inputShortnames[i]), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return err - } - if err := db.DB.Where(&models.ObligationMap{ObligationPk: obligation.Id, RfPk: license.Id}).First(&obligationMap).Error; err != nil { - // License not in map, add to insert slice - if errors.Is(err, gorm.ErrRecordNotFound) { - *insertLicenseIds = append(*insertLicenseIds, license.Id) - } else { - er := models.LicenseError{ - Status: http.StatusInternalServerError, - Message: fmt.Sprintf("unable to fetch obligation maps for obligation with topic '%s'", obligation.Topic), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusInternalServerError, er) - return err - } - } - // License in request, remove from removal slice - *removeLicenseIds = removeFromSlice(*removeLicenseIds, license.Id) + return } - return nil -} + obMapInput.Shortnames = slices.Compact(obMapInput.Shortnames) -// PerformObligationMapActions performs the actions for ObligationMap endpoint PATCH and PUT calls. -// It takes the input of obligation which is being modified, list of licenses to be removed and added, -// and the user making the changes. The function computes the changelog and returns the response. -func PerformObligationMapActions(username string, obligation models.Obligation, removeLicenseIds []int64, - insertLicenseIds []int64) (*models.ObligationMapResponse, error) { - var oldObMaps []models.ObligationMap - var newObMaps []models.ObligationMap - var removeObMaps []models.ObligationMap - var insertObMaps []models.ObligationMap - - if err := db.DB.Where(models.ObligationMap{ObligationPk: obligation.Id}).Find(&oldObMaps).Error; err != nil { - return nil, err - } + utils.GenerateDiffForReplacingLicenses(&obligation, obMapInput.Shortnames, &removeLicenses, &insertLicenses) - for i := 0; i < len(removeLicenseIds); i++ { - deleteItem := models.ObligationMap{ - ObligationPk: obligation.Id, - RfPk: removeLicenseIds[i], - } - // Find the primary key to make delete faster - if err := db.DB.Where(&deleteItem).First(&deleteItem).Error; err != nil { - return nil, err - } - removeObMaps = append(removeObMaps, deleteItem) - } - for i := 0; i < len(insertLicenseIds); i++ { - insertObMaps = append(insertObMaps, models.ObligationMap{ - ObligationPk: obligation.Id, - RfPk: insertLicenseIds[i], - }) - } - - if err := db.DB.Transaction(func(tx *gorm.DB) error { - if len(removeObMaps) > 0 { - // Bulk delete removeObMaps from DB - if err := tx.Delete(&removeObMaps).Error; err != nil { - return err - } - } - if len(insertObMaps) > 0 { - // Bulk create insertObMaps in DB - if err := tx.Create(&insertObMaps).Error; err != nil { - return err + username := c.GetString("username") + newLicenseAssociations, errs := utils.PerformObligationMapActions(username, &obligation, removeLicenses, insertLicenses) + if len(errs) != 0 { + var combinedErrors string + for _, err := range errs { + if err != nil { + combinedErrors += fmt.Sprintf("%s\n", err) } } - - if err := tx.Where(models.ObligationMap{ObligationPk: obligation.Id}).Find(&newObMaps).Error; err != nil { - return err + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Unable to add/remove some of the licenses", + Error: combinedErrors, + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), } - - return createObligationMapChangelog(tx, username, oldObMaps, newObMaps, &obligation) - - }); err != nil { - return nil, err + c.JSON(http.StatusInternalServerError, er) + return } - obMap, err := createObligationMapUser(obligation, newObMaps) - if err != nil { - return nil, err + obMap := &models.ObligationMapUser{ + Topic: *obligation.Topic, + Type: (*obligation.Type).Type, + Shortnames: newLicenseAssociations, } res := models.ObligationMapResponse{ @@ -479,70 +306,5 @@ func PerformObligationMapActions(username string, obligation models.Obligation, }, } - return &res, nil -} - -// createObligationMapChangelog creates the changelog for the obligation map changes. -func createObligationMapChangelog(tx *gorm.DB, username string, oldObMaps, newObMaps []models.ObligationMap, obligation *models.Obligation) error { - var oldLicenses []string - var newLicenses []string - - for i := 0; i < len(oldObMaps); i++ { - oldLicenses = append(oldLicenses, strconv.FormatInt(oldObMaps[i].RfPk, 10)) - } - for i := 0; i < len(newObMaps); i++ { - newLicenses = append(newLicenses, strconv.FormatInt(newObMaps[i].RfPk, 10)) - } - - oldVal := strings.Join(oldLicenses, ",") - newVal := strings.Join(newLicenses, ",") - change := models.ChangeLog{ - Field: "RfPk", - OldValue: &oldVal, - UpdatedValue: &newVal, - } - - var user models.User - if err := tx.Where(models.User{Username: username}).First(&user).Error; err != nil { - return err - } - - audit := models.Audit{ - UserId: user.Id, - TypeId: obligation.Id, - Timestamp: time.Now(), - Type: "license", - ChangeLogs: []models.ChangeLog{change}, - } - - if err := tx.Create(&audit).Error; err != nil { - return err - } - - return nil -} - -// removeFromSlice removes the item from the slice if it exists. -func removeFromSlice[E string | int64](slice []E, item E) []E { - if slices.Contains(slice, item) { - return append(slice[:slices.Index(slice, item)], slice[slices.Index(slice, item)+1:]...) - } - return slice -} - -// createObligationMapUser creates the response data for the obligation map endpoint. -func createObligationMapUser(obligation models.Obligation, obMaps []models.ObligationMap) (*models.ObligationMapUser, error) { - var shortnameList []string - for i := 0; i < len(obMaps); i++ { - var license models.LicenseDB - if err := db.DB.Where(models.LicenseDB{Id: obMaps[i].RfPk}).First(&license).Error; err != nil { - return nil, err - } - shortnameList = append(shortnameList, *license.Shortname) - } - return &models.ObligationMapUser{ - Topic: obligation.Topic, - Type: obligation.Type, - Shortnames: shortnameList, - }, nil + c.JSON(http.StatusOK, res) } diff --git a/pkg/api/obligations.go b/pkg/api/obligations.go index 267b095..2e68abf 100644 --- a/pkg/api/obligations.go +++ b/pkg/api/obligations.go @@ -1,16 +1,15 @@ // SPDX-FileCopyrightText: 2023 Kavya Shukla // SPDX-FileCopyrightText: 2023 Siemens AG // SPDX-FileContributor: Gaurav Mishra +// SPDX-FileContributor: Dearsh Oberoi // // SPDX-License-Identifier: GPL-2.0-only package api import ( - "crypto/md5" - "encoding/hex" + "context" "encoding/json" - "errors" "fmt" "net/http" "path/filepath" @@ -18,6 +17,8 @@ import ( "strings" "time" + "golang.org/x/exp/slices" + "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" "github.com/fossology/LicenseDb/pkg/utils" @@ -38,7 +39,7 @@ import ( // @Param page query int false "Page number" // @Param limit query int false "Number of records per page" // @Param order_by query string false "Asc or desc ordering" Enums(asc, desc) default(asc) -// @Success 200 {object} models.ObligationResponse +// @Success 200 {object} models.SwaggerObligationResponse // @Failure 404 {object} models.LicenseError "No obligations in DB" // @Security ApiKeyAuth || {} // @Router /obligations [get] @@ -75,7 +76,7 @@ func GetAllObligation(c *gin.Context) { query.Order(queryOrderString) - if err = query.Find(&obligations).Error; err != nil { + if err = query.Joins("Type").Joins("Classification").Preload("Licenses").Find(&obligations).Error; err != nil { er := models.LicenseError{ Status: http.StatusInternalServerError, Message: "Unable to fetch obligations", @@ -86,11 +87,12 @@ func GetAllObligation(c *gin.Context) { c.JSON(http.StatusInternalServerError, er) return } + res := models.ObligationResponse{ Data: obligations, Status: http.StatusOK, Meta: &models.PaginationMeta{ - ResourceCount: len(obligations), + ResourceCount: 0, }, } @@ -106,7 +108,7 @@ func GetAllObligation(c *gin.Context) { // @Accept json // @Produce json // @Param topic path string true "Topic of the obligation" -// @Success 200 {object} models.ObligationResponse +// @Success 200 {object} models.SwaggerObligationResponse // @Failure 404 {object} models.LicenseError "No obligation with given topic found" // @Security ApiKeyAuth || {} // @Router /obligations/{topic} [get] @@ -114,7 +116,7 @@ func GetObligation(c *gin.Context) { var obligation models.Obligation query := db.DB.Model(&obligation) tp := c.Param("topic") - if err := query.Where(models.Obligation{Topic: tp}).First(&obligation).Error; err != nil { + if err := query.Joins("Type").Joins("Classification").Preload("Licenses").Where(models.Obligation{Topic: &tp}).First(&obligation).Error; err != nil { er := models.LicenseError{ Status: http.StatusNotFound, Message: fmt.Sprintf("obligation with topic '%s' not found", tp), @@ -143,17 +145,17 @@ func GetObligation(c *gin.Context) { // @Tags Obligations // @Accept json // @Produce json -// @Param obligation body models.ObligationPOSTRequestJSONSchema true "Obligation to create" -// @Success 201 {object} models.ObligationResponse +// @Param obligation body models.ObligationDTO true "Obligation to create" +// @Success 201 {object} models.SwaggerObligationResponse // @Failure 400 {object} models.LicenseError "Bad request body" // @Failure 409 {object} models.LicenseError "Obligation with same body exists" // @Failure 500 {object} models.LicenseError "Unable to create obligation" // @Security ApiKeyAuth // @Router /obligations [post] func CreateObligation(c *gin.Context) { - var input models.ObligationPOSTRequestJSONSchema + var obligation models.Obligation - if err := c.ShouldBindJSON(&input); err != nil { + if err := c.ShouldBindJSON(&obligation); err != nil { er := models.LicenseError{ Status: http.StatusBadRequest, Message: "invalid json body", @@ -164,39 +166,12 @@ func CreateObligation(c *gin.Context) { c.JSON(http.StatusBadRequest, er) return } - s := input.Text - hash := md5.Sum([]byte(s)) - md5hash := hex.EncodeToString(hash[:]) - - obligation := models.Obligation{ - Md5: md5hash, - Type: input.Type, - Topic: input.Topic, - Text: input.Text, - Classification: input.Classification, - Comment: input.Comment, - Modifications: input.Modifications, - Active: input.Active, - TextUpdatable: false, - } result := db.DB. Where(&models.Obligation{Topic: obligation.Topic}). Or(&models.Obligation{Md5: obligation.Md5}). FirstOrCreate(&obligation) - if result.RowsAffected == 0 { - er := models.LicenseError{ - Status: http.StatusConflict, - Message: "can not create obligation with same topic or text", - Error: fmt.Sprintf("Error: Obligation with topic '%s' or Text '%s'... already exists", - obligation.Topic, obligation.Text[0:10]), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusConflict, er) - return - } if result.Error != nil { er := models.LicenseError{ Status: http.StatusInternalServerError, @@ -208,14 +183,18 @@ func CreateObligation(c *gin.Context) { c.JSON(http.StatusInternalServerError, er) return } - for _, i := range input.Shortnames { - var license models.LicenseDB - db.DB.Where(models.LicenseDB{Shortname: &i}).Find(&license) - obmap := models.ObligationMap{ - ObligationPk: obligation.Id, - RfPk: license.Id, + + if result.RowsAffected == 0 { + er := models.LicenseError{ + Status: http.StatusConflict, + Message: "can not create obligation with same topic or text", + Error: fmt.Sprintf("Error: Obligation with topic '%s' or Text '%s'... already exists", + *obligation.Topic, (*obligation.Text)[0:10]), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), } - db.DB.Create(&obmap) + c.JSON(http.StatusConflict, er) + return } res := models.ObligationResponse{ @@ -237,161 +216,85 @@ func CreateObligation(c *gin.Context) { // @Tags Obligations // @Accept json // @Produce json -// @Param topic path string true "Topic of the obligation to be updated" -// @Param obligation body models.ObligationPATCHRequestJSONSchema true "Obligation to be updated" -// @Success 200 {object} models.ObligationResponse +// @Param topic path string true "Topic of the obligation to be updated" +// @Param obligation body models.ObligationUpdateDTO true "Obligation to be updated" +// @Success 200 {object} models.SwaggerObligationResponse // @Failure 400 {object} models.LicenseError "Invalid request" // @Failure 404 {object} models.LicenseError "No obligation with given topic found" // @Failure 500 {object} models.LicenseError "Unable to update obligation" // @Security ApiKeyAuth // @Router /obligations/{topic} [patch] func UpdateObligation(c *gin.Context) { - _ = db.DB.Transaction(func(tx *gorm.DB) error { - var updates models.ObligationPATCHRequestJSONSchema - var oldObligation models.Obligation - newObligationMap := make(map[string]interface{}) - username := c.GetString("username") - tp := c.Param("topic") - - if err := tx.Model(&oldObligation).Where(models.Obligation{Topic: tp}).First(&oldObligation).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: fmt.Sprintf("obligation with topic '%s' not found", tp), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return err - } - - if err := c.ShouldBindJSON(&updates); err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "invalid json body", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return err - } - - if updates.Text.IsDefined { - if updates.Text.Value == "" { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "Text cannot be an empty string", - Error: "invalid request", - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return errors.New("invalid request") - } - - updatedHash := md5.Sum([]byte(updates.Text.Value)) - updatedMd5hash := hex.EncodeToString(updatedHash[:]) - if !oldObligation.TextUpdatable { - if updatedMd5hash != oldObligation.Md5 { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "Can not update obligation text", - Error: "invalid request", - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return errors.New("invalid request") - } - } - newObligationMap["md5"] = updatedMd5hash - newObligationMap["text"] = updates.Text.Value - } - - if updates.Type.IsDefined { - if updates.Type.Value == "" { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "Type cannot be an empty string", - Error: "invalid request", - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return errors.New("invalid request") - } - newObligationMap["type"] = updates.Type.Value - } - - if updates.Classification.IsDefined { - if updates.Classification.Value == "" { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "Classification cannot be an empty string", - Error: "invalid request", - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return errors.New("invalid request") - } - newObligationMap["classification"] = updates.Classification.Value - } + var updates models.ObligationUpdateDTO + var oldObligation models.Obligation + username := c.GetString("username") + tp := c.Param("topic") - if updates.Modifications.IsDefined { - newObligationMap["modifications"] = updates.Modifications.Value + if err := db.DB.Joins("Classification").Joins("Type").Preload("Licenses").Where(models.Obligation{Topic: &tp}).First(&oldObligation).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: fmt.Sprintf("obligation with topic '%s' not found", tp), + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), } + c.JSON(http.StatusNotFound, er) + return + } - if updates.Comment.IsDefined { - newObligationMap["comment"] = updates.Comment.Value + if err := c.ShouldBindJSON(&updates); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "invalid json body", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), } + c.JSON(http.StatusBadRequest, er) + return + } - if updates.Active.IsDefined { - newObligationMap["active"] = updates.Active.Value - } + newObligation := updates.Converter() + newObligation.Id = oldObligation.Id - if updates.TextUpdatable.IsDefined { - newObligationMap["text_updatable"] = updates.TextUpdatable.Value + 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 { + return err } - var newObligation models.Obligation - newObligation.Id = oldObligation.Id - if err := tx.Model(&newObligation).Clauses(clause.Returning{}).Updates(newObligationMap).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) + if err := tx.Joins("Type").Joins("Classification").First(&newObligation).Error; err != nil { return err } - if err := addChangelogsForObligationUpdate(tx, username, &newObligation, &oldObligation); 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) + if err := addChangelogsForObligationUpdate(tx, username, newObligation, &oldObligation); err != nil { return err } - res := models.ObligationResponse{ - Data: []models.Obligation{newObligation}, - Status: http.StatusOK, - Meta: &models.PaginationMeta{ - ResourceCount: 1, - }, - } - c.JSON(http.StatusOK, res) + newObligation.Licenses = oldObligation.Licenses return nil - }) + }); 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 + } + + res := models.ObligationResponse{ + Data: []models.Obligation{*newObligation}, + Status: http.StatusOK, + Meta: &models.PaginationMeta{ + ResourceCount: 1, + }, + } + c.JSON(http.StatusOK, res) } // DeleteObligation marks an existing obligation record as inactive @@ -410,7 +313,7 @@ func UpdateObligation(c *gin.Context) { func DeleteObligation(c *gin.Context) { var obligation models.Obligation tp := c.Param("topic") - if err := db.DB.Where(models.Obligation{Topic: tp}).First(&obligation).Error; err != nil { + if err := db.DB.Where(models.Obligation{Topic: &tp}).First(&obligation).Error; err != nil { er := models.LicenseError{ Status: http.StatusNotFound, Message: fmt.Sprintf("obligation with topic '%s' not found", tp), @@ -421,34 +324,44 @@ func DeleteObligation(c *gin.Context) { c.JSON(http.StatusNotFound, er) return } - obligation.Active = false - db.DB.Where(models.Obligation{Topic: tp}).Save(&obligation) + *obligation.Active = false + if err := db.DB.Session(&gorm.Session{SkipHooks: true}).Updates(&obligation).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "failed to delete obligation", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return + } c.Status(http.StatusNoContent) } // GetObligationAudits fetches audits corresponding to an obligation - -// @Summary Fetches audits corresponding to an obligation -// @Description Fetches audits corresponding to an obligation -// @Id GetObligationAudits -// @Tags Obligations -// @Accept json -// @Produce json -// @Param topic path string true "Topic of the obligation for which audits need to be fetched" -// @Param page query int false "Page number" -// @Param limit query int false "Number of records per page" -// @Success 200 {object} models.AuditResponse -// @Failure 404 {object} models.LicenseError "No obligation with given topic found" -// @Failure 500 {object} models.LicenseError "unable to find audits with such obligation topic" +// +// @Summary Fetches audits corresponding to an obligation +// @Description Fetches audits corresponding to an obligation +// @Id GetObligationAudits +// @Tags Obligations +// @Accept json +// @Produce json +// @Param topic path string true "Topic of the obligation for which audits need to be fetched" +// @Param page query int false "Page number" +// @Param limit query int false "Number of records per page" +// @Success 200 {object} models.AuditResponse +// @Failure 404 {object} models.LicenseError "No obligation with given topic found" +// @Failure 500 {object} models.LicenseError "unable to find audits with such obligation topic" // // @Security ApiKeyAuth || {} // -// @Router /obligations/{topic}/audits [get] +// @Router /obligations/{topic}/audits [get] func GetObligationAudits(c *gin.Context) { var obligation models.Obligation topic := c.Param("topic") - result := db.DB.Where(models.Obligation{Topic: topic}).Select("id").First(&obligation) + result := db.DB.Where(models.Obligation{Topic: &topic}).Select("id").First(&obligation) if result.Error != nil { er := models.LicenseError{ Status: http.StatusNotFound, @@ -462,8 +375,8 @@ func GetObligationAudits(c *gin.Context) { } var audits []models.Audit - query := db.DB.Model(&models.Audit{}) - query.Where(models.Audit{TypeId: obligation.Id, Type: "Obligation"}) + query := db.DB.Model(&models.Audit{}).Preload("User") + query.Where(models.Audit{TypeId: obligation.Id, Type: "Obligation"}).Order("timestamp desc") _ = utils.PreparePaginateResponse(c, query, &models.AuditResponse{}) res := query.Find(&audits) @@ -479,6 +392,20 @@ func GetObligationAudits(c *gin.Context) { return } + for i := 0; i < len(audits); i++ { + if err := utils.GetAuditEntity(c, &audits[i]); err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "unable to find audits with such obligation topic", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return + } + } + response := models.AuditResponse{ Data: audits, Status: http.StatusOK, @@ -532,7 +459,7 @@ func ImportObligations(c *gin.Context) { return } - var obligations []models.ObligationJSONFileFormat + var obligations []models.Obligation decoder := json.NewDecoder(file) if err := decoder.Decode(&obligations); err != nil { er := models.LicenseError{ @@ -550,63 +477,42 @@ func ImportObligations(c *gin.Context) { Status: http.StatusOK, } - for _, obligation := range obligations { + for _, ob := range obligations { + oldObligation := ob + newObligation := ob _ = db.DB.Transaction(func(tx *gorm.DB) error { - ob := models.Obligation{ - Topic: obligation.Topic, - Type: obligation.Type, - Text: obligation.Text, - Classification: obligation.Classification, - Modifications: obligation.Modifications, - Comment: obligation.Comment, - Active: obligation.Active, - TextUpdatable: obligation.TextUpdatable, - } - - hash := md5.Sum([]byte(ob.Text)) - md5hash := hex.EncodeToString(hash[:]) - ob.Md5 = md5hash - - oldObligation := ob result := tx. - Where(&models.Obligation{Topic: ob.Topic}). - Or(&models.Obligation{Md5: ob.Md5}). + Joins("Type"). + Joins("Classification"). + Preload("Licenses"). + Where(&models.Obligation{Topic: oldObligation.Topic}). + Or(&models.Obligation{Md5: oldObligation.Md5}). + Omit("Licenses"). FirstOrCreate(&oldObligation) if result.Error != nil { res.Data = append(res.Data, models.LicenseError{ Status: http.StatusInternalServerError, - Message: fmt.Sprintf("Failed to create obligation: %s", result.Error.Error()), - Error: ob.Topic, + Message: fmt.Sprintf("Failed to create/update obligation: %s", result.Error.Error()), + Error: *ob.Topic, Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), }) return err } else if result.RowsAffected == 0 { // case when obligation exists in database and is updated - result := tx.Model(&ob).Clauses(clause.Returning{}).Where(&models.Obligation{Topic: ob.Topic}).Updates(&ob) - if result.Error != nil { + newObligation.Id = oldObligation.Id + ctx := context.WithValue(context.Background(), models.ContextKey("oldObligation"), &oldObligation) + if err := tx.WithContext(ctx).Omit("Licenses", "Topic").Clauses(clause.Returning{}).Updates(&newObligation).Error; err != nil { res.Data = append(res.Data, models.LicenseError{ Status: http.StatusInternalServerError, - Message: fmt.Sprintf("Failed to update obligation: %s", result.Error.Error()), - Error: ob.Topic, - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - }) - return err - } - - if result.RowsAffected == 0 { - res.Data = append(res.Data, models.LicenseError{ - Status: http.StatusConflict, - Message: "Another obligation with the same text exists", - Error: ob.Topic, + Message: fmt.Sprintf("Failed to update obligation: %s", err.Error()), + Error: *ob.Topic, Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), }) return err } - - if err := addChangelogsForObligationUpdate(tx, username, &ob, &oldObligation); err != nil { + if err := addChangelogsForObligationUpdate(tx, username, &newObligation, &oldObligation); err != nil { res.Data = append(res.Data, models.LicenseError{ Status: http.StatusInternalServerError, Message: "Failed to update license", @@ -618,20 +524,54 @@ func ImportObligations(c *gin.Context) { } res.Data = append(res.Data, models.ObligationImportStatus{ - Data: models.ObligationId{Id: ob.Id, Topic: ob.Topic}, - Status: http.StatusOK, + Data: models.ObligationId{Id: ob.Id, Topic: *ob.Topic}, + Status: http.StatusOK, + Message: "obligation updated successfully", }) } else { // case when obligation doesn't exist in database and is inserted res.Data = append(res.Data, models.ObligationImportStatus{ - Data: models.ObligationId{Id: oldObligation.Id, Topic: oldObligation.Topic}, - Status: http.StatusCreated, + Data: models.ObligationId{Id: oldObligation.Id, Topic: *oldObligation.Topic}, + Status: http.StatusCreated, + Message: "obligation created successfully", }) } return nil }) + + var shortnames, removeLicenses, insertLicenses []string + for _, lic := range ob.Licenses { + shortnames = append(shortnames, *lic.Shortname) + } + shortnames = slices.Compact(shortnames) + + utils.GenerateDiffForReplacingLicenses(&oldObligation, shortnames, &removeLicenses, &insertLicenses) + + username := c.GetString("username") + _, errs := utils.PerformObligationMapActions(username, &oldObligation, removeLicenses, insertLicenses) + if len(errs) != 0 { + var combinedErrors string + for _, err := range errs { + if err != nil { + combinedErrors += fmt.Sprintf("%s\n", err) + } + } + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Error updating license associations", + Error: combinedErrors, + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + } else { + res.Data = append(res.Data, models.ObligationImportStatus{ + Data: models.ObligationId{Id: ob.Id, Topic: *ob.Topic}, + Status: http.StatusOK, + Message: "licenses associated with obligations successfully", + }) + } } c.JSON(http.StatusOK, res) @@ -644,15 +584,14 @@ func ImportObligations(c *gin.Context) { // @Id ExportObligations // @Tags Obligations // @Produce json -// @Success 200 {array} models.ObligationJSONFileFormat +// @Success 200 {array} models.ObligationDTO // @Failure 500 {object} models.LicenseError "Failed to fetch obligations" // @Security ApiKeyAuth || {} // @Router /obligations/export [get] func ExportObligations(c *gin.Context) { var obligations []models.Obligation - var obligationsJSONFileFormat []models.ObligationJSONFileFormat - if err := db.DB.Model(&models.Obligation{}).Find(&obligations).Error; err != nil { + if err := db.DB.Joins("Type").Joins("Classification").Preload("Licenses").Find(&obligations).Error; err != nil { er := models.LicenseError{ Status: http.StatusInternalServerError, Message: "Failed to fetch obligations", @@ -664,40 +603,6 @@ func ExportObligations(c *gin.Context) { return } - for _, obligation := range obligations { - var obligationMaps []models.ObligationMap - if err := db.DB.Model(&obligationMaps).Preload("LicenseDB").Where(models.ObligationMap{ObligationPk: obligation.Id}).Find(&obligationMaps).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusInternalServerError, - Message: "Failed to fetch obligations", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusInternalServerError, er) - return - } - - var shortnames []string - for _, obMap := range obligationMaps { - shortnames = append(shortnames, *obMap.LicenseDB.Shortname) - } - - obJSONFileFormat := models.ObligationJSONFileFormat{ - Topic: obligation.Topic, - Type: obligation.Type, - Text: obligation.Text, - Shortnames: shortnames, - TextUpdatable: obligation.TextUpdatable, - Active: obligation.Active, - Modifications: obligation.Modifications, - Comment: obligation.Comment, - Classification: obligation.Classification, - } - - obligationsJSONFileFormat = append(obligationsJSONFileFormat, obJSONFileFormat) - } - fileName := strings.Map(func(r rune) rune { if r == '+' || r == ':' { return '_' @@ -706,7 +611,7 @@ func ExportObligations(c *gin.Context) { }, fmt.Sprintf("obligations-export-%s.json", time.Now().Format(time.RFC3339))) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName)) - c.JSON(http.StatusOK, &obligationsJSONFileFormat) + c.JSON(http.StatusOK, &obligations) } // addChangelogsForObligationUpdate adds changelogs for the updated fields on obligation update @@ -717,63 +622,55 @@ func addChangelogsForObligationUpdate(tx *gorm.DB, username string, return err } var changes []models.ChangeLog - - if oldObligation.Topic != newObligation.Topic { - changes = append(changes, models.ChangeLog{ - Field: "Topic", - OldValue: &oldObligation.Topic, - UpdatedValue: &newObligation.Topic, - }) - } - if oldObligation.Type != newObligation.Type { + if (*oldObligation.Type).Type != (*newObligation.Type).Type { changes = append(changes, models.ChangeLog{ Field: "Type", - OldValue: &oldObligation.Type, - UpdatedValue: &newObligation.Type, + OldValue: &(*oldObligation.Type).Type, + UpdatedValue: &(*newObligation.Type).Type, }) } if oldObligation.Md5 != newObligation.Md5 { changes = append(changes, models.ChangeLog{ Field: "Text", - OldValue: &oldObligation.Text, - UpdatedValue: &newObligation.Text, + OldValue: oldObligation.Text, + UpdatedValue: newObligation.Text, }) } - if oldObligation.Classification != newObligation.Classification { + if (*oldObligation.Classification).Classification != (*newObligation.Classification).Classification { changes = append(changes, models.ChangeLog{ Field: "Classification", - OldValue: &oldObligation.Classification, - UpdatedValue: &newObligation.Classification, + OldValue: &(*oldObligation.Classification).Classification, + UpdatedValue: &(*newObligation.Classification).Classification, }) } - if oldObligation.Modifications != newObligation.Modifications { - oldVal := strconv.FormatBool(oldObligation.Modifications) - newVal := strconv.FormatBool(newObligation.Modifications) + if *oldObligation.Modifications != *newObligation.Modifications { + oldVal := strconv.FormatBool(*oldObligation.Modifications) + newVal := strconv.FormatBool(*newObligation.Modifications) changes = append(changes, models.ChangeLog{ Field: "Modifications", OldValue: &oldVal, UpdatedValue: &newVal, }) } - if oldObligation.Comment != newObligation.Comment { + if *oldObligation.Comment != *newObligation.Comment { changes = append(changes, models.ChangeLog{ Field: "Comment", - OldValue: &oldObligation.Comment, - UpdatedValue: &newObligation.Comment, + OldValue: oldObligation.Comment, + UpdatedValue: newObligation.Comment, }) } - if oldObligation.Active != newObligation.Active { - oldVal := strconv.FormatBool(oldObligation.Active) - newVal := strconv.FormatBool(newObligation.Active) + if *oldObligation.Active != *newObligation.Active { + oldVal := strconv.FormatBool(*oldObligation.Active) + newVal := strconv.FormatBool(*newObligation.Active) changes = append(changes, models.ChangeLog{ Field: "Active", OldValue: &oldVal, UpdatedValue: &newVal, }) } - if oldObligation.TextUpdatable != newObligation.TextUpdatable { - oldVal := strconv.FormatBool(oldObligation.TextUpdatable) - newVal := strconv.FormatBool(newObligation.TextUpdatable) + if *oldObligation.TextUpdatable != *newObligation.TextUpdatable { + oldVal := strconv.FormatBool(*oldObligation.TextUpdatable) + newVal := strconv.FormatBool(*newObligation.TextUpdatable) changes = append(changes, models.ChangeLog{ Field: "TextUpdatable", OldValue: &oldVal, @@ -830,10 +727,8 @@ func GetAllObligationPreviews(c *gin.Context) { c.JSON(http.StatusBadRequest, er) return } - query := db.DB.Model(&models.Obligation{}) - query.Where("active = ?", parsedActive) - if err = query.Find(&obligations).Error; err != nil { + if err = db.DB.Joins("Type").Where("active = ?", parsedActive).Find(&obligations).Error; err != nil { er := models.LicenseError{ Status: http.StatusInternalServerError, Message: "Unable to fetch obligations", @@ -847,8 +742,8 @@ func GetAllObligationPreviews(c *gin.Context) { for _, ob := range obligations { obligationPreviews = append(obligationPreviews, models.ObligationPreview{ - Topic: ob.Topic, - Type: ob.Type, + Topic: *ob.Topic, + Type: (*ob.Type).Type, }) } diff --git a/pkg/db/db.go b/pkg/db/db.go index 4f199a6..892ac69 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -4,17 +4,11 @@ package db import ( - "encoding/json" - "errors" "fmt" "log" - "os" "gorm.io/driver/postgres" "gorm.io/gorm" - - "github.com/fossology/LicenseDb/pkg/models" - "github.com/fossology/LicenseDb/pkg/utils" ) // DB is a global variable to store the GORM database connection. @@ -32,32 +26,3 @@ func Connect(dbhost, port, user, dbname, password *string) { DB = database } - -// Populatedb populates the database with license data from a JSON file. -func Populatedb(datafile string) { - var licenses []models.LicenseJson - // Read the content of the data file. - byteResult, err := os.ReadFile(datafile) - if err != nil { - log.Fatalf("Unable to read JSON file: %v", err) - } - // Unmarshal the JSON file data into a slice of LicenseJson structs. - if err := json.Unmarshal(byteResult, &licenses); err != nil { - log.Fatalf("error reading from json file: %v", err) - } - for _, license := range licenses { - result := utils.Converter(license) - _ = DB.Transaction(func(tx *gorm.DB) error { - errMessage, importStatus, _, _ := utils.InsertOrUpdateLicenseOnImport(tx, &result, &models.UpdateExternalRefsJSONPayload{ExternalRef: make(map[string]interface{})}) - if importStatus == utils.IMPORT_FAILED { - // ANSI escape code for red text - red := "\033[31m" - reset := "\033[0m" - log.Printf("%s%s: %s%s", red, *result.Shortname, errMessage, reset) - return errors.New(errMessage) - } - return nil - }) - - } -} diff --git a/pkg/models/optional_data_types.go b/pkg/models/optional_data_types.go deleted file mode 100644 index 46e9eab..0000000 --- a/pkg/models/optional_data_types.go +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Siemens AG -// SPDX-FileContributor: Dearsh Oberoi -// -// SPDX-License-Identifier: GPL-2.0-only - -package models - -import ( - "encoding/json" - "errors" -) - -// When we unmarshal json, the undefined keys take zero values in structs. So, there -// is no way to differentiate between an undefined value and an actual zero value when -// it is passed. OptionalData is a generic for differentiating between undefined and -// zero valued keys in json. -type OptionalData[T any] struct { - // This is set to true if corresponding key is present in json object - IsDefined bool - rawJson json.RawMessage - Value T -} - -func (v *OptionalData[T]) UnmarshalJSON(data []byte) error { - v.rawJson = append((v.rawJson)[0:0], data...) - if len(v.rawJson) != 0 { - var x *T - if err := json.Unmarshal(data, &x); err != nil { - return err - } - if x == nil { - return errors.New("field value cannot be null") - } - v.Value = *x - v.IsDefined = true - } - return nil -} - -type NullableAndOptionalData[T any] struct { - // This is set to true if corresponding key is present in json object - IsDefinedAndNotNull bool - rawJson json.RawMessage - Value T -} - -func (v *NullableAndOptionalData[T]) UnmarshalJSON(data []byte) error { - v.rawJson = append((v.rawJson)[0:0], data...) - if len(v.rawJson) != 0 { - var x *T - if err := json.Unmarshal(data, &x); err != nil { - return err - } - if x != nil { - v.Value = *x - v.IsDefinedAndNotNull = true - } - } - return nil -} diff --git a/pkg/models/types.go b/pkg/models/types.go index 717662c..0b03331 100644 --- a/pkg/models/types.go +++ b/pkg/models/types.go @@ -1,15 +1,21 @@ // SPDX-FileCopyrightText: 2023 Kavya Shukla // SPDX-FileCopyrightText: 2023 Siemens AG // SPDX-FileContributor: Gaurav Mishra +// SPDX-FileContributor: Dearsh Oberoi // // SPDX-License-Identifier: GPL-2.0-only package models import ( + "crypto/md5" + "encoding/hex" + "encoding/json" "errors" + "fmt" "time" + "github.com/go-playground/validator/v10" "gorm.io/datatypes" "gorm.io/gorm" ) @@ -40,6 +46,7 @@ type LicenseDB struct { Flag *int64 `json:"flag" gorm:"default:1;column:rf_flag;not null;default:0" validate:"omitempty,min=0,max=2" example:"1"` Marydone *bool `json:"marydone" gorm:"column:marydone;not null;default:false"` ExternalRef datatypes.JSONType[LicenseDBSchemaExtension] `json:"external_ref"` + Obligations []*Obligation `gorm:"many2many:obligation_licenses;" json:"obligations"` } func (l *LicenseDB) BeforeSave(tx *gorm.DB) (err error) { @@ -91,6 +98,7 @@ type LicenseUpdateJSONSchema struct { Flag *int64 `json:"flag" validate:"omitempty,min=0,max=2" example:"1"` Marydone *bool `json:"marydone" example:"false"` ExternalRef datatypes.JSONType[LicenseDBSchemaExtension] `json:"external_ref"` + Obligations []*Obligation `json:"obligations"` } // UpdateExternalRefsJSONPayload struct represents the external ref key value pairs for update @@ -271,18 +279,267 @@ type AuditResponse struct { Meta *PaginationMeta `json:"paginationmeta"` } +type ObligationType struct { + Id int64 `gorm:"primary_key"` + Type string `gorm:"unique;not null"` +} + +type ObligationClassification struct { + Id int64 `gorm:"primary_key"` + Classification string `gorm:"unique;not null"` + Color string `gorm:"unique; not null"` +} + // Obligation represents an obligation record in the database. type Obligation struct { - Id int64 `gorm:"primary_key" json:"id" example:"147"` - Topic string `gorm:"unique" json:"topic" example:"copyleft"` - Type string `json:"type" enums:"obligation,restriction,risk,right" example:"risk"` - Text string `json:"text" example:"Source code be made available when distributing the software."` - Classification string `json:"classification" enums:"green,white,yellow,red" example:"green"` - Modifications bool `json:"modifications" example:"true"` - Comment string `json:"comment"` - Active bool `json:"active"` - TextUpdatable bool `json:"text_updatable" example:"true"` - Md5 string `gorm:"unique" json:"-"` + Id int64 `gorm:"primary_key"` + Topic *string `gorm:"unique;not null"` + Text *string `gorm:"not null"` + Modifications *bool `gorm:"not null;default:false"` + Comment *string `gorm:"not null;default:''"` + Active *bool `gorm:"not null;default:true"` + TextUpdatable *bool `gorm:"not null;default:false"` + Md5 string `gorm:"unique;not null"` + ObligationClassificationId int64 `gorm:"not null"` + ObligationTypeId int64 `gorm:"not null"` + Licenses []*LicenseDB `gorm:"many2many:obligation_licenses;"` + Type *ObligationType `gorm:"foreignKey:ObligationTypeId"` + Classification *ObligationClassification `gorm:"foreignKey:ObligationClassificationId"` +} + +func (o *Obligation) BeforeCreate(tx *gorm.DB) (err error) { + if o.Topic != nil && *o.Topic == "" { + return errors.New("topic cannot be an empty string") + } + // Checks whether the obligation type value passed on by the user is a valid value or not + // i.e. it should be already present in the obligation_type table. Then the object queried + // from the database is assigned to the Type field because it has primary key. Objects passed + // on without primary key are first saved into db and then foreignkey references are saved. + if o.Type != nil { + var obTypes []ObligationType + if err := tx.Find(&obTypes).Error; err != nil { + return err + } + allTypes := "" + for i := 0; i < len(obTypes); i++ { + allTypes += fmt.Sprintf(" %s", obTypes[i].Type) + if o.Type.Type == obTypes[i].Type { + o.Type = &obTypes[i] + } + } + if o.Type.Id == 0 { + return fmt.Errorf("obligation type must be one of the following values:%s", allTypes) + } + } + if o.Text != nil { + if *o.Text == "" { + return errors.New("text cannot be an empty string") + } else { + s := *o.Text + hash := md5.Sum([]byte(s)) + md5hash := hex.EncodeToString(hash[:]) + o.Md5 = md5hash + } + } + if o.Classification != nil { + var obClassifications []ObligationClassification + if err := tx.Find(&obClassifications).Error; err != nil { + return err + } + allClassifications := "" + for i := 0; i < len(obClassifications); i++ { + allClassifications += fmt.Sprintf(" %s", obClassifications[i].Classification) + if o.Classification.Classification == obClassifications[i].Classification { + o.Classification = &obClassifications[i] + } + } + if o.Classification.Id == 0 { + return fmt.Errorf("obligation classification must be one of the following values:%s", allClassifications) + } + } + + for i := 0; i < len(o.Licenses); i++ { + var license LicenseDB + if err := tx.Where(LicenseDB{Shortname: o.Licenses[i].Shortname}).First(&license).Error; err != nil { + return fmt.Errorf("license with shortname %s not found", *o.Licenses[i].Shortname) + } + o.Licenses[i] = &license + } + + return +} + +type ContextKey string + +func (o *Obligation) BeforeUpdate(tx *gorm.DB) (err error) { + + oldObligation, ok := tx.Statement.Context.Value(ContextKey("oldObligation")).(*Obligation) + if !ok { + return errors.New("something went wrong") + } + + if o.Topic != nil && *o.Topic == "" { + return errors.New("topic cannot be an empty string") + } + + if o.Type != nil { + var obTypes []ObligationType + if err := tx.Find(&obTypes).Error; err != nil { + return err + } + allTypes := "" + for i := 0; i < len(obTypes); i++ { + allTypes += fmt.Sprintf(" %s", obTypes[i].Type) + if o.Type.Type == obTypes[i].Type { + o.Type = &obTypes[i] + } + } + if o.Type.Id == 0 { + return fmt.Errorf("obligation type must be one of the following values:%s", allTypes) + } + } + if o.Text != nil { + if *o.Text == "" { + return errors.New("text cannot be an empty string") + } else { + hash := md5.Sum([]byte(*o.Text)) + o.Md5 = hex.EncodeToString(hash[:]) + if !*oldObligation.TextUpdatable { + if o.Md5 != oldObligation.Md5 { + return errors.New("can not update obligation text") + } + } + } + } + if o.Classification != nil { + var obClassifications []ObligationClassification + if err := tx.Find(&obClassifications).Error; err != nil { + return err + } + allClassifications := "" + for i := 0; i < len(obClassifications); i++ { + allClassifications += fmt.Sprintf(" %s", obClassifications[i].Classification) + if o.Classification.Classification == obClassifications[i].Classification { + o.Classification = &obClassifications[i] + } + } + if o.Classification.Id == 0 { + return fmt.Errorf("obligation classification must be one of the following values:%s", allClassifications) + } + } + return +} + +// Custom json marshaller and unmarshaller for Obligation +func (o *Obligation) MarshalJSON() ([]byte, error) { + ob := ObligationDTO{ + Topic: o.Topic, + Text: o.Text, + Modifications: o.Modifications, + Comment: o.Comment, + Active: o.Active, + TextUpdatable: o.TextUpdatable, + Shortnames: []string{}, + } + + if o.Type != nil { + ob.Type = &o.Type.Type + } + + if o.Classification != nil { + ob.Classification = &o.Classification.Classification + } + + for i := 0; i < len(o.Licenses); i++ { + ob.Shortnames = append(ob.Shortnames, *o.Licenses[i].Shortname) + } + return json.Marshal(ob) +} + +// Custom JSON unmarshaller for Obligation +func (o *Obligation) UnmarshalJSON(data []byte) error { + var dto ObligationDTO + + if err := json.Unmarshal(data, &dto); err != nil { + return err + } + + validate := validator.New(validator.WithRequiredStructEnabled()) + if err := validate.Struct(&dto); err != nil { + return fmt.Errorf("field '%s' failed validation: %s", err.(validator.ValidationErrors)[0].Field(), err.(validator.ValidationErrors)[0].Tag()) + } + + o.Topic = dto.Topic + o.Text = dto.Text + o.Modifications = dto.Modifications + o.Comment = dto.Comment + o.Active = dto.Active + o.TextUpdatable = dto.TextUpdatable + + if dto.Type != nil { + o.Type = &ObligationType{ + Type: *dto.Type, + } + } + + if dto.Classification != nil { + o.Classification = &ObligationClassification{ + Classification: *dto.Classification, + } + } + + o.Licenses = []*LicenseDB{} + for i := 0; i < len(dto.Shortnames); i++ { + o.Licenses = append(o.Licenses, &LicenseDB{ + Shortname: &dto.Shortnames[i], + }) + } + + 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"` +} + +// 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"` +} + +func (obDto *ObligationUpdateDTO) Converter() *Obligation { + var o Obligation + + o.Topic = obDto.Topic + if obDto.Type != nil { + o.Type = &ObligationType{Type: *obDto.Type} + } + o.Text = obDto.Text + if obDto.Classification != nil { + o.Classification = &ObligationClassification{Classification: *obDto.Classification} + } + o.Modifications = obDto.Modifications + o.Comment = obDto.Comment + o.Active = obDto.Active + o.TextUpdatable = obDto.TextUpdatable + + return &o } // ObligationPreview is just the Type and Topic of Obligation @@ -297,29 +554,6 @@ type ObligationPreviewResponse struct { Data []ObligationPreview `json:"data"` } -// ObligationPOSTRequestJSONSchema represents the data format of POST request for obligation -type ObligationPOSTRequestJSONSchema struct { - Topic string `json:"topic" binding:"required" example:"copyleft"` - Type string `json:"type" enums:"obligation,restriction,risk,right" binding:"required"` - Text string `json:"text" binding:"required" example:"Source code be made available when distributing the software."` - Classification string `json:"classification" enums:"green,white,yellow,red" binding:"required"` - Modifications bool `json:"modifications" binding:"required"` - Comment string `json:"comment" binding:"required"` - Shortnames []string `json:"shortnames" binding:"required" example:"GPL-2.0-only,GPL-2.0-or-later"` - Active bool `json:"active" binding:"required" example:"true"` -} - -// ObligationPATCHRequestJSONSchema represents the data format of PATCH request for obligation -type ObligationPATCHRequestJSONSchema struct { - Type OptionalData[string] `json:"type" swaggertype:"string" enums:"obligation,restriction,risk,right"` - Text OptionalData[string] `json:"text" swaggertype:"string" example:"Source code be made available when distributing the software."` - Classification OptionalData[string] `json:"classification" swaggertype:"string" enums:"green,white,yellow,red"` - Modifications OptionalData[bool] `json:"modifications" swaggertype:"boolean"` - Comment OptionalData[string] `json:"comment" swaggertype:"string" example:"This is a comment."` - Active OptionalData[bool] `json:"active" swaggertype:"boolean" example:"true"` - TextUpdatable OptionalData[bool] `json:"text_updatable" swaggertype:"boolean"` -} - // ObligationResponse represents the response format for obligation data. type ObligationResponse struct { Status int `json:"status" example:"200"` @@ -327,13 +561,11 @@ type ObligationResponse struct { Meta *PaginationMeta `json:"paginationmeta"` } -// ObligationMap represents the mapping between an obligation and a license. -type ObligationMap struct { - ObligationPk int64 `json:"obligation_pk"` - Obligation Obligation `gorm:"foreignKey:ObligationPk;references:Id" json:"-"` - OmPk int64 `json:"om_pk" gorm:"primary_key"` - RfPk int64 `json:"rf_pk"` - LicenseDB LicenseDB `gorm:"foreignKey:RfPk;references:Id" json:"-"` +// SwaggerObligationResponse represents the response format for obligation data. +type SwaggerObligationResponse struct { + Status int `json:"status" example:"200"` + Data []ObligationDTO `json:"data"` + Meta *PaginationMeta `json:"paginationmeta"` } // ObligationMapUser Structure with obligation topic and license shortname list, a simple representation for user. @@ -371,19 +603,6 @@ type ObligationImportRequest struct { ObligationFile string `form:"file"` } -// ObligationJSONFileFormat represents an obligation record in the import/export json file. -type ObligationJSONFileFormat struct { - Topic string `json:"topic" example:"copyleft" validate:"required"` // binding:"required" tag cannot be used as is works only for request body - Type string `json:"type" enums:"obligation,restriction,risk,right" validate:"required"` - Text string `json:"text" example:"Source code be made available when distributing the software." validate:"required"` - Classification string `json:"classification" enums:"green,white,yellow,red" validate:"required"` - Modifications bool `json:"modifications" validate:"required"` - Comment string `json:"comment" example:"This is a comment." validate:"required"` - Active bool `json:"active" validate:"required"` - TextUpdatable bool `json:"text_updatable" validate:"required"` - Shortnames []string `json:"shortnames" example:"GPL-2.0-only,GPL-2.0-or-later" validate:"required"` -} - // ObligationId is the id of successfully imported obligation type ObligationId struct { Id int64 `json:"id" example:"31"` @@ -392,8 +611,9 @@ type ObligationId struct { // ObligationImportStatus is the status of obligation records successfully inserted in the database during import type ObligationImportStatus struct { - Status int `json:"status" example:"200"` - Data ObligationId `json:"data"` + Status int `json:"status" example:"200"` + Data ObligationId `json:"data"` + Message string `json:"message"` } // ImportObligationsResponse is the response structure for import obligation response diff --git a/pkg/utils/util.go b/pkg/utils/util.go index ea43ba6..4144d92 100644 --- a/pkg/utils/util.go +++ b/pkg/utils/util.go @@ -1,15 +1,20 @@ // SPDX-FileCopyrightText: 2023 Kavya Shukla // SPDX-FileCopyrightText: 2023 Siemens AG // SPDX-FileContributor: Gaurav Mishra +// SPDX-FileContributor: Dearsh Oberoi // // SPDX-License-Identifier: GPL-2.0-only package utils import ( + "encoding/json" + "errors" "fmt" "html" + "log" "net/http" + "os" "strconv" "strings" "time" @@ -22,6 +27,7 @@ import ( "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" + "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" ) @@ -260,3 +266,186 @@ func InsertOrUpdateLicenseOnImport(tx *gorm.DB, license *models.LicenseDB, exter return message, importStatus, &oldLicense, &newLicense } + +// GenerateDiffForReplacingLicenses creates list of license associations to be created and deleted such that the list of currently associated +// licenses to a obligation is overwritten by the list provided in the param newLicenseAssociations +func GenerateDiffForReplacingLicenses(obligation *models.Obligation, newLicenseAssociations []string, removeLicenses, insertLicenses *[]string) { + // if license in currently associated with the obligation but isn't in newLicenseAssociations, remove it + for _, lic := range obligation.Licenses { + found := false + for _, shortname := range newLicenseAssociations { + if shortname == *lic.Shortname { + found = true + break + } + } + if !found { + *removeLicenses = append(*removeLicenses, *lic.Shortname) + } + } + + // if license in newLicenseAssociations but not currently associated with the obligation, insert it + for _, shortname := range newLicenseAssociations { + found := false + for _, lic := range obligation.Licenses { + if shortname == *lic.Shortname { + found = true + break + } + } + if !found { + *insertLicenses = append(*insertLicenses, shortname) + } + } +} + +// PerformObligationMapActions created associations for licenses in insertLicenses and deletes +// associations for licenses in removeLicenses. It also calculates changelog for the changes. +// It returns the final list of associated licenses. +func PerformObligationMapActions(username string, obligation *models.Obligation, + removeLicenses, insertLicenses []string) ([]string, []error) { + createLicenseAssociations := []models.LicenseDB{} + deleteLicenseAssociations := []models.LicenseDB{} + var oldLicenseAssociations, newLicenseAssociations []string + var errs []error + + for _, lic := range obligation.Licenses { + oldLicenseAssociations = append(oldLicenseAssociations, *lic.Shortname) + } + + for _, lic := range insertLicenses { + var license models.LicenseDB + if err := db.DB.Where(models.LicenseDB{Shortname: &lic}).First(&license).Error; err != nil { + errs = append(errs, fmt.Errorf("unable to associate license '%s' to obligation '%s': %s", lic, *obligation.Topic, err.Error())) + } else { + createLicenseAssociations = append(createLicenseAssociations, license) + } + } + + for _, lic := range removeLicenses { + var license models.LicenseDB + if err := db.DB.Where(models.LicenseDB{Shortname: &lic}).First(&license).Error; err != nil { + errs = append(errs, fmt.Errorf("unable to remove license '%s' from obligation '%s': %s", lic, *obligation.Topic, err.Error())) + } else { + deleteLicenseAssociations = append(deleteLicenseAssociations, license) + } + } + + if err := db.DB.Transaction(func(tx *gorm.DB) error { + + if len(createLicenseAssociations) == 0 && len(deleteLicenseAssociations) == 0 { + for _, lic := range obligation.Licenses { + newLicenseAssociations = append(newLicenseAssociations, *lic.Shortname) + } + return nil + } + + if err := tx.Session(&gorm.Session{SkipHooks: true}).Model(obligation).Association("Licenses").Append(createLicenseAssociations); err != nil { + return err + } + if err := tx.Session(&gorm.Session{SkipHooks: true}).Model(obligation).Association("Licenses").Delete(deleteLicenseAssociations); err != nil { + return err + } + + for _, lic := range obligation.Licenses { + newLicenseAssociations = append(newLicenseAssociations, *lic.Shortname) + } + + return createObligationMapChangelog(tx, username, newLicenseAssociations, oldLicenseAssociations, obligation) + }); err != nil { + errs = append(errs, err) + } + return newLicenseAssociations, errs +} + +// createObligationMapChangelog creates the changelog for the obligation map changes. +func createObligationMapChangelog(tx *gorm.DB, username string, + newLicenseAssociations, oldLicenseAssociations []string, obligation *models.Obligation) error { + oldVal := strings.Join(oldLicenseAssociations, ", ") + newVal := strings.Join(newLicenseAssociations, ", ") + change := models.ChangeLog{ + Field: "Licenses", + OldValue: &oldVal, + UpdatedValue: &newVal, + } + + var user models.User + if err := tx.Where(models.User{Username: username}).First(&user).Error; err != nil { + return err + } + + audit := models.Audit{ + UserId: user.Id, + TypeId: obligation.Id, + Timestamp: time.Now(), + Type: "obligation", + ChangeLogs: []models.ChangeLog{change}, + } + + if err := tx.Create(&audit).Error; err != nil { + return err + } + + return nil +} + +// Populatedb populates the database with license data from a JSON file. +func Populatedb(datafile string) { + var licenses []models.LicenseJson + // Read the content of the data file. + byteResult, err := os.ReadFile(datafile) + if err != nil { + log.Fatalf("Unable to read JSON file: %v", err) + } + // Unmarshal the JSON file data into a slice of LicenseJson structs. + if err := json.Unmarshal(byteResult, &licenses); err != nil { + log.Fatalf("error reading from json file: %v", err) + } + for _, license := range licenses { + result := Converter(license) + _ = db.DB.Transaction(func(tx *gorm.DB) error { + errMessage, importStatus, _, _ := InsertOrUpdateLicenseOnImport(tx, &result, &models.UpdateExternalRefsJSONPayload{ExternalRef: make(map[string]interface{})}) + if importStatus == IMPORT_FAILED { + // ANSI escape code for red text + red := "\033[31m" + reset := "\033[0m" + log.Printf("%s%s: %s%s", red, *result.Shortname, errMessage, reset) + return errors.New(errMessage) + } + return nil + }) + + } +} + +// GetAuditEntity is an utility function to fetch obligation or license associated with an audit +func GetAuditEntity(c *gin.Context, audit *models.Audit) error { + if audit.Type == "license" || audit.Type == "License" { + audit.Entity = &models.LicenseDB{} + if err := db.DB.Where(&models.LicenseDB{Id: audit.TypeId}).First(&audit.Entity).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: "license corresponding with this audit does not exist", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return err + } + } else if audit.Type == "obligation" || audit.Type == "Obligation" { + audit.Entity = &models.Obligation{} + if err := db.DB.Joins("Type").Joins("Classification").Where(&models.Obligation{Id: audit.TypeId}).First(&audit.Entity).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: "obligation corresponding with this audit does not exist", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return err + } + } + return nil +}