Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2556 from transcom/ren-167443089-update-deletion-…
…to-soft-delete Update uploads/documents deletion to use soft delete
- Loading branch information
Showing
32 changed files
with
648 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 5 additions & 0 deletions
5
migrations/20190730225048_add_deleted_at_to_document_tables.up.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
ALTER TABLE documents ADD COLUMN deleted_at timestamp with time zone; | ||
ALTER TABLE move_documents ADD COLUMN deleted_at timestamp with time zone; | ||
ALTER TABLE uploads ADD COLUMN deleted_at timestamp with time zone; | ||
ALTER TABLE weight_ticket_set_documents ADD COLUMN deleted_at timestamp with time zone; | ||
ALTER TABLE moving_expense_documents ADD COLUMN deleted_at timestamp with time zone; |
5 changes: 5 additions & 0 deletions
5
migrations/20190829152347_create-deleted-at-indices-for-document-tables.up.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
CREATE INDEX documents_deleted_at_idx ON documents USING btree(deleted_at); | ||
CREATE INDEX move_documents_deleted_at_idx ON move_documents USING btree(deleted_at); | ||
CREATE INDEX uploads_deleted_at_idx ON uploads USING btree(deleted_at); | ||
CREATE INDEX weight_ticket_set_documents_deleted_at_idx ON weight_ticket_set_documents USING btree(deleted_at); | ||
CREATE INDEX moving_expense_documents_deleted_at_idx ON moving_expense_documents USING btree(deleted_at); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package utilities | ||
|
||
import ( | ||
"errors" | ||
"reflect" | ||
"time" | ||
|
||
"github.com/gobuffalo/pop" | ||
"github.com/gobuffalo/validate" | ||
|
||
"github.com/gofrs/uuid" | ||
) | ||
|
||
const deletedAt = "DeletedAt" | ||
const modelsPkgPath = "github.com/transcom/mymove/pkg/models" | ||
|
||
// SoftDestroy soft deletes a record and all foreign key associations from the database | ||
func SoftDestroy(c *pop.Connection, model interface{}) error { | ||
verrs := validate.NewErrors() | ||
var err error | ||
|
||
if !IsModel(model) { | ||
return errors.New("can only soft delete type model") | ||
} | ||
|
||
modelValue := reflect.ValueOf(model).Elem() | ||
deletedAtField := modelValue.FieldByName(deletedAt) | ||
|
||
if deletedAtField.IsValid() { | ||
if deletedAtField.CanSet() { | ||
now := time.Now() | ||
reflectTime := reflect.ValueOf(&now) | ||
deletedAtField.Set(reflectTime) | ||
verrs, err = c.ValidateAndSave(model) | ||
|
||
if err != nil || verrs.HasAny() { | ||
return errors.New("error updating model") | ||
} | ||
} else { | ||
return errors.New("can not soft delete this model") | ||
} | ||
} else { | ||
return errors.New("this model does not have deleted_at field") | ||
} | ||
|
||
associations := GetForeignKeyAssociations(c, model) | ||
if len(associations) > 0 { | ||
for _, association := range associations { | ||
err = SoftDestroy(c, association) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// IsModel verifies if the given interface is a model | ||
func IsModel(model interface{}) bool { | ||
pkgPath := reflect.TypeOf(model).Elem().PkgPath() | ||
return pkgPath == modelsPkgPath | ||
} | ||
|
||
// GetForeignKeyAssociations fetches all the foreign key associations the model has | ||
func GetForeignKeyAssociations(c *pop.Connection, model interface{}) []interface{} { | ||
var foreignKeyAssociations []interface{} | ||
c.Load(model) | ||
|
||
modelValue := reflect.ValueOf(model).Elem() | ||
modelType := modelValue.Type() | ||
|
||
for pos := 0; pos < modelValue.NumField(); pos++ { | ||
fieldValue := modelValue.Field(pos) | ||
|
||
if fieldValue.CanInterface() { | ||
association := fieldValue.Interface() | ||
|
||
if association != nil { | ||
hasOneTag := modelType.Field(pos).Tag.Get("has_one") | ||
hasManyTag := modelType.Field(pos).Tag.Get("has_many") | ||
|
||
if hasOneTag != "" && GetHasOneForeignKeyAssociation(association) != nil { | ||
foreignKeyAssociations = append(foreignKeyAssociations, association) | ||
} | ||
|
||
if hasManyTag != "" { | ||
foreignKeyAssociations = append(foreignKeyAssociations, GetHasManyForeignKeyAssociations(association)...) | ||
} | ||
} | ||
} | ||
} | ||
return foreignKeyAssociations | ||
} | ||
|
||
// GetHasOneForeignKeyAssociation fetches the "has_one" foreign key association if not an empty model | ||
func GetHasOneForeignKeyAssociation(model interface{}) interface{} { | ||
modelValue := reflect.ValueOf(model).Elem() | ||
idField := modelValue.FieldByName("ID") | ||
|
||
if idField.CanInterface() && idField.Interface() != uuid.Nil { | ||
return model | ||
} | ||
return nil | ||
} | ||
|
||
// GetHasManyForeignKeyAssociations fetches the "has_many" foreing key association if not an empty model | ||
func GetHasManyForeignKeyAssociations(model interface{}) []interface{} { | ||
var hasManyForeignKeyAssociations []interface{} | ||
associations := reflect.ValueOf(model) | ||
|
||
for pos := 0; pos < associations.Len(); pos++ { | ||
association := associations.Index(pos) | ||
idField := association.FieldByName("ID") | ||
|
||
if idField.CanInterface() && idField.Interface() != uuid.Nil { | ||
associationPtr := association.Addr().Interface() | ||
hasManyForeignKeyAssociations = append(hasManyForeignKeyAssociations, associationPtr) | ||
} | ||
} | ||
return hasManyForeignKeyAssociations | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
package utilities_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/suite" | ||
|
||
"github.com/transcom/mymove/pkg/db/utilities" | ||
"github.com/transcom/mymove/pkg/models" | ||
"github.com/transcom/mymove/pkg/services/mocks" | ||
"github.com/transcom/mymove/pkg/testdatagen" | ||
"github.com/transcom/mymove/pkg/testingsuite" | ||
"github.com/transcom/mymove/pkg/unit" | ||
) | ||
|
||
type UtilitiesSuite struct { | ||
testingsuite.PopTestSuite | ||
} | ||
|
||
func (suite *UtilitiesSuite) SetupTest() { | ||
suite.DB().TruncateAll() | ||
} | ||
|
||
func TestUtilitiesSuite(t *testing.T) { | ||
hs := &UtilitiesSuite{ | ||
PopTestSuite: testingsuite.NewPopTestSuite(testingsuite.CurrentPackage()), | ||
} | ||
suite.Run(t, hs) | ||
} | ||
|
||
func (suite *UtilitiesSuite) TestSoftDestroy_NotModel() { | ||
accessCodeFetcher := &mocks.AccessCodeFetcher{} | ||
|
||
err := utilities.SoftDestroy(suite.DB(), &accessCodeFetcher) | ||
|
||
suite.Equal("can only soft delete type model", err.Error()) | ||
} | ||
|
||
func (suite *UtilitiesSuite) TestSoftDestroy_ModelWithoutDeletedAtWithoutAssociations() { | ||
//model without deleted_at with no associations | ||
user := testdatagen.MakeDefaultUser(suite.DB()) | ||
|
||
err := utilities.SoftDestroy(suite.DB(), &user) | ||
|
||
suite.Equal("this model does not have deleted_at field", err.Error()) | ||
} | ||
|
||
func (suite *UtilitiesSuite) TestSoftDestroy_ModelWithDeletedAtWithoutAssociations() { | ||
//model with deleted_at with no associations | ||
expenseDocumentModel := testdatagen.MakeMovingExpenseDocument(suite.DB(), testdatagen.Assertions{ | ||
MovingExpenseDocument: models.MovingExpenseDocument{ | ||
MovingExpenseType: models.MovingExpenseTypeCONTRACTEDEXPENSE, | ||
PaymentMethod: "GTCC", | ||
RequestedAmountCents: unit.Cents(10000), | ||
}, | ||
}) | ||
|
||
suite.MustSave(&expenseDocumentModel) | ||
suite.Nil(expenseDocumentModel.DeletedAt) | ||
|
||
err := utilities.SoftDestroy(suite.DB(), &expenseDocumentModel) | ||
suite.NoError(err) | ||
} | ||
|
||
func (suite *UtilitiesSuite) TestSoftDestroy_ModelWithoutDeletedAtWithAssociations() { | ||
// model without deleted_at with associations | ||
serviceMember := testdatagen.MakeDefaultServiceMember(suite.DB()) | ||
suite.MustSave(&serviceMember) | ||
|
||
err := utilities.SoftDestroy(suite.DB(), &serviceMember) | ||
suite.Equal("this model does not have deleted_at field", err.Error()) | ||
} | ||
|
||
func (suite *UtilitiesSuite) TestSoftDestroy_ModelWithDeletedAtWithHasOneAssociations() { | ||
// model with deleted_at with "has one" associations | ||
ppm := testdatagen.MakePPM(suite.DB(), testdatagen.Assertions{ | ||
PersonallyProcuredMove: models.PersonallyProcuredMove{ | ||
Status: models.PPMStatusPAYMENTREQUESTED, | ||
}, | ||
}) | ||
move := ppm.Move | ||
moveDoc := testdatagen.MakeMoveDocument(suite.DB(), | ||
testdatagen.Assertions{ | ||
MoveDocument: models.MoveDocument{ | ||
MoveID: move.ID, | ||
Move: move, | ||
PersonallyProcuredMoveID: &ppm.ID, | ||
MoveDocumentType: models.MoveDocumentTypeWEIGHTTICKETSET, | ||
Status: models.MoveDocumentStatusOK, | ||
}, | ||
}) | ||
suite.MustSave(&moveDoc) | ||
suite.Nil(moveDoc.DeletedAt) | ||
|
||
emptyWeight := unit.Pound(1000) | ||
fullWeight := unit.Pound(2500) | ||
weightTicketSetDocument := models.WeightTicketSetDocument{ | ||
MoveDocumentID: moveDoc.ID, | ||
MoveDocument: moveDoc, | ||
EmptyWeight: &emptyWeight, | ||
EmptyWeightTicketMissing: false, | ||
FullWeight: &fullWeight, | ||
FullWeightTicketMissing: false, | ||
VehicleNickname: "My Car", | ||
VehicleOptions: "CAR", | ||
WeightTicketDate: &testdatagen.NextValidMoveDate, | ||
TrailerOwnershipMissing: false, | ||
} | ||
suite.MustSave(&weightTicketSetDocument) | ||
suite.Nil(weightTicketSetDocument.DeletedAt) | ||
|
||
err := utilities.SoftDestroy(suite.DB(), &moveDoc) | ||
|
||
suite.NoError(err) | ||
suite.NotNil(moveDoc.DeletedAt) | ||
suite.NotNil(moveDoc.WeightTicketSetDocument.DeletedAt) | ||
} | ||
|
||
func (suite *UtilitiesSuite) TestSoftDestroy_ModelWithDeletedAtWithHasManyAssociations() { | ||
// model with deleted_at with "has many" associations | ||
serviceMember := testdatagen.MakeDefaultServiceMember(suite.DB()) | ||
|
||
document := testdatagen.MakeDocument(suite.DB(), testdatagen.Assertions{ | ||
Document: models.Document{ | ||
ServiceMemberID: serviceMember.ID, | ||
ServiceMember: serviceMember, | ||
}, | ||
}) | ||
suite.MustSave(&document) | ||
suite.Nil(document.DeletedAt) | ||
|
||
upload := models.Upload{ | ||
DocumentID: &document.ID, | ||
UploaderID: document.ServiceMember.UserID, | ||
Filename: "test.pdf", | ||
Bytes: 1048576, | ||
ContentType: "application/pdf", | ||
Checksum: "ImGQ2Ush0bDHsaQthV5BnQ==", | ||
} | ||
upload2 := models.Upload{ | ||
DocumentID: &document.ID, | ||
UploaderID: document.ServiceMember.UserID, | ||
Filename: "test2.pdf", | ||
Bytes: 1048576, | ||
ContentType: "application/pdf", | ||
Checksum: "ImGQ2Ush0bDHsaQthV5BnQ==", | ||
} | ||
suite.MustSave(&upload) | ||
suite.MustSave(&upload2) | ||
suite.Nil(upload.DeletedAt) | ||
suite.Nil(upload2.DeletedAt) | ||
|
||
err := utilities.SoftDestroy(suite.DB(), &document) | ||
|
||
suite.NoError(err) | ||
suite.NotNil(document.DeletedAt) | ||
suite.NotNil(document.Uploads[0].DeletedAt) | ||
suite.NotNil(document.Uploads[1].DeletedAt) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.