3
0

Refactor record update/create procedures

This commit is contained in:
Denis Arh 2020-02-25 08:18:56 +01:00
parent b9a49645f3
commit 4d72fe1a13
2 changed files with 131 additions and 86 deletions

View File

@ -326,6 +326,43 @@ func (svc record) Export(filter types.RecordFilter, enc Encoder) error {
}
func (svc record) Create(new *types.Record) (rec *types.Record, err error) {
var invokerID = auth.GetIdentityFromContext(svc.ctx).Identity()
// Runs value sanitization, sets values that should used
// and validates the final result
//
// This logic is kept in a utility function - it's used in the beginning
// of the creation procedure and after results are back from the automation scripts
//
// Both these points introduce external data that need to be checked fully in the same manner
procChanges := func(m *types.Module, new *types.Record) *types.RecordValueErrorSet {
// Before values are processed further and
// sent to automation scripts (if any)
// we need to make sure it does not get sanitized data
new.Values = svc.sanitizer.Run(m, new.Values)
// Reset values to new record
// to make sure nobody slips in something we do not want
new.CreatedBy = invokerID
new.CreatedAt = *nowPtr()
new.UpdatedAt = nil
new.UpdatedBy = 0
new.DeletedAt = nil
new.DeletedBy = 0
// Mark all values as updated (new)
new.Values.SetUpdatedFlag(true)
if new.OwnedBy == 0 {
// If od owner is not set, make current user
// the owner of the record
new.OwnedBy = invokerID
}
// Run validation of the updated records
return svc.validator.Run(m, new)
}
return rec, svc.db.Transaction(func() (err error) {
var (
ns *types.Namespace
@ -345,24 +382,12 @@ func (svc record) Create(new *types.Record) (rec *types.Record, err error) {
return
}
// First sanitization
//
// Before values are sent to automation scripts (if any)
// we need to make sure it does not get sanitized data
creatorID := auth.GetIdentityFromContext(svc.ctx).Identity()
var (
rve *types.RecordValueErrorSet
)
new.OwnedBy = creatorID
new.CreatedBy = creatorID
new.CreatedAt = time.Now()
new.UpdatedAt = nil
new.UpdatedBy = 0
new.DeletedAt = nil
new.UpdatedBy = 0
new.Values = svc.sanitizer.Run(m, new.Values)
// Before values are stored, we have to validate them
rve := values.Validator().Run(m, new)
if !rve.IsValid() {
// Handle input payload
if rve = procChanges(m, new); !rve.IsValid() {
return rve
}
@ -375,49 +400,80 @@ func (svc record) Create(new *types.Record) (rec *types.Record, err error) {
// Assign defaults (only on missing values)
new.Values = svc.setDefaultValues(m, new.Values)
if new.OwnedBy == 0 {
// Allow ownership change
new.OwnedBy = creatorID
}
// Reconstruct the final record, and re-sanitize everything
// we could use new, but we do not trust before-create scripts
r := &types.Record{
ModuleID: new.ModuleID,
NamespaceID: new.NamespaceID,
CreatedBy: creatorID,
OwnedBy: new.OwnedBy,
CreatedAt: time.Now(),
Values: svc.sanitizer.Run(m, new.Values),
}
// Before values are stored, we have to validate them
if rve = svc.validator.Run(m, r); !rve.IsValid() {
// Handle payload from automation scripts
if rve = procChanges(m, new); !rve.IsValid() {
return rve
}
if r, err = svc.recordRepo.Create(r); err != nil {
if new, err = svc.recordRepo.Create(new); err != nil {
return
}
if err = svc.recordRepo.UpdateValues(r.ID, r.Values); err != nil {
if err = svc.recordRepo.UpdateValues(new.ID, new.Values); err != nil {
return
}
// At this point we can return the value
rec = r
defer svc.eventbus.Dispatch(svc.ctx, event.RecordAfterCreate(r, nil, m, ns, nil))
rec = new
defer svc.eventbus.Dispatch(svc.ctx, event.RecordAfterCreateImmutable(new, nil, m, ns, nil))
return
})
}
func (svc record) Update(upd *types.Record) (rec *types.Record, err error) {
var invokerID = auth.GetIdentityFromContext(svc.ctx).Identity()
// Runs value sanitization, copies values that should updated
// and validates the final result
//
// This logic is kept in a utility function - it's used in the beginning
// of the update procedure and after results are back from the automation scripts
//
// Both these points introduce external data that need to be checked fully in the same manner
procChanges := func(m *types.Module, upd *types.Record, old *types.Record) *types.RecordValueErrorSet {
// First sanitization
//
// Before values are merged with existing data and
// sent to automation scripts (if any)
// we need to make sure it does not get sanitized data
upd.Values = svc.sanitizer.Run(m, upd.Values)
// Copy values to updated record
// to make sure nobody slips in something we do not want
upd.CreatedAt = old.CreatedAt
upd.CreatedBy = old.CreatedBy
upd.UpdatedAt = nowPtr()
upd.UpdatedBy = invokerID
upd.DeletedAt = old.DeletedAt
upd.DeletedBy = old.DeletedBy
// Merge new (updated) values with old ones
// This way we get list of updated, stale and deleted values
// that we can selectively updated in the repository
upd.Values = old.Values.Merge(upd.Values)
if upd.OwnedBy == 0 && old.OwnedBy > 0 {
// Owner not set/send in the payload
//
// Fallback to old owner (if set)
upd.OwnedBy = old.OwnedBy
} else {
// If od owner is not set, make current user
// the owner of the record
upd.OwnedBy = invokerID
}
// Run validation of the updated records
return svc.validator.Run(m, upd)
}
return rec, svc.db.Transaction(func() (err error) {
if upd.ID == 0 {
return ErrInvalidID.withStack()
}
ns, m, r, err := svc.loadCombo(upd.NamespaceID, upd.ModuleID, upd.ID)
ns, m, old, err := svc.loadCombo(upd.NamespaceID, upd.ModuleID, upd.ID)
if err != nil {
return
}
@ -426,73 +482,57 @@ func (svc record) Update(upd *types.Record) (rec *types.Record, err error) {
return
}
// Test if stale (update has an older version of data)
if isStale(upd.UpdatedAt, old.UpdatedAt, old.CreatedAt) {
return ErrStaleData.withStack()
}
if !svc.ac.CanUpdateRecord(svc.ctx, m) {
return ErrNoUpdatePermissions.withStack()
}
// Test if stale (update has an older version of data)
if isStale(upd.UpdatedAt, r.UpdatedAt, r.CreatedAt) {
return ErrStaleData.withStack()
}
// First sanitization
//
// Before values are merged with existing data and
// sent to automation scripts (if any)
// we need to make sure it does not get sanitized data
upd.CreatedAt = r.CreatedAt
upd.CreatedBy = r.CreatedBy
upd.UpdatedAt = r.UpdatedAt
upd.UpdatedBy = r.UpdatedBy
upd.DeletedAt = r.DeletedAt
upd.UpdatedBy = r.UpdatedBy
upd.Values = svc.sanitizer.Run(m, upd.Values)
if upd.OwnedBy == 0 {
upd.OwnedBy = r.OwnedBy
}
// Before values are stored, we have to validate them
rve := svc.validator.Run(m, upd)
if !rve.IsValid() {
return rve
}
// Preload old record values so we can send it together with event
if err = svc.preloadValues(m, r); err != nil {
if err = svc.preloadValues(m, old); err != nil {
return
}
if err = svc.eventbus.WaitFor(svc.ctx, event.RecordBeforeUpdate(upd, r, m, ns, rve)); err != nil {
var (
rve *types.RecordValueErrorSet
)
// Handle input payload
if rve = procChanges(m, upd, old); !rve.IsValid() {
return rve
}
// Scripts can (besides simple error value) return complex record value error set
// that is passed back to the UI or any other API consumer
//
// rve (record-validation-errorset) struct is passed so it can be
// used & filled by automation scripts
if err = svc.eventbus.WaitFor(svc.ctx, event.RecordBeforeUpdate(upd, old, m, ns, rve)); err != nil {
return
} else if !rve.IsValid() {
return rve
}
svc.recordInfoUpdate(r)
// Sanitize values we got from before-update automation scripts
r.Values = svc.sanitizer.Run(m, upd.Values)
if upd.OwnedBy > 0 {
// Allow ownership change:
r.OwnedBy = upd.OwnedBy
}
// Before values are stored, we have to validate them
if rve = svc.validator.Run(m, r); !rve.IsValid() {
// Handle payload from automation scripts
if rve = procChanges(m, upd, old); !rve.IsValid() {
return rve
}
if r, err = svc.recordRepo.Update(r); err != nil {
if upd, err = svc.recordRepo.Update(upd); err != nil {
return
}
if err = svc.recordRepo.UpdateValues(r.ID, r.Values); err != nil {
if err = svc.recordRepo.UpdateValues(upd.ID, upd.Values); err != nil {
return
}
// At this point we can return the value
rec = r
defer svc.eventbus.Dispatch(svc.ctx, event.RecordAfterUpdate(upd, r, m, ns, nil))
rec = upd
defer svc.eventbus.Dispatch(svc.ctx, event.RecordAfterUpdateImmutable(upd, old, m, ns, nil))
return
})
}
@ -519,7 +559,7 @@ func (svc record) DeleteByID(namespaceID, moduleID uint64, recordIDs ...uint64)
var (
isBulkDelete = len(recordIDs) > 0
now = time.Now()
now = *nowPtr()
ns *types.Namespace
m *types.Module
@ -578,7 +618,7 @@ func (svc record) DeleteByID(namespaceID, moduleID uint64, recordIDs ...uint64)
return err
}
defer svc.eventbus.Dispatch(svc.ctx, event.RecordAfterDelete(nil, del, m, ns, nil))
defer svc.eventbus.Dispatch(svc.ctx, event.RecordAfterDeleteImmutable(nil, del, m, ns, nil))
return err
})

View File

@ -169,3 +169,8 @@ func isStale(new *time.Time, updatedAt *time.Time, createdAt time.Time) bool {
return new.Equal(createdAt)
}
func nowPtr() *time.Time {
now := time.Now()
return &now
}