aip-go icon indicating copy to clipboard operation
aip-go copied to clipboard

fieldmask: use field behavior for applying updates

Open nlachfr opened this issue 2 years ago • 4 comments

Hello,

The AIP-203 specification defines custom behavior for updates using field masks :

  • a field with (google.api.field_behavior) = OUTPUT_ONLY should be ignored
  • a field with (google.api.field_behavior) = IMMUTABLE should raise an error when the field is modified

At the moment, these behaviors are not taken into account by the fieldmask module and I think that it would be nice if it was supported.

What do you think about adding it in the fieldmask.Update(mask *fieldmaskpb.FieldMask, dst, src proto.Message) function ?

nlachfr avatar Apr 12 '22 16:04 nlachfr

Hello everyone,

Just a friendly comment to revive this issue :eyes: Implementing this feature would allow us to write fully AIP-compliant update methods very easily like so:

func (s *iamSvc) UpdateUser(ctx context.Context, in *iamv1.UpdateUserRequest) (*iamv1.User, error) {
  // You must first validate and authorize the request but it is out of scope
  now := timestamppb.Now()
  user, err := s.usersRepo.GetUser(ctx, in.User.Name)
  if err != nil {
    return nil, err
  }
  if err := fieldmask.Update(in.UpdateMask, user, in.User); err != nil {
    // An error is thrown when an IMMUTABLE constraint is violated
    return nil, errors.Wrap(err, errors.CodeInvalidArgument, "invalid update mask")
  }
  // user.CreateTime is not updated because of the OUTPUT_ONLY constraint
  user.UpdateTime = now
  if err := s.usersRepo.SaveUser(ctx, user); err != nil {
    return nil, err
  }
  return user, nil
}

With very little changes you can transform the method above into an upsert:

func (s *iamSvc) UpdateUser(ctx context.Context, in *iamv1.UpdateUserRequest) (*iamv1.User, error) {
  // You must first validate and authorize the request but it is out of scope
  now := timestamppb.Now()
  user, err := s.usersRepo.GetUser(ctx, in.User.Name)
  if err != nil {
    if errors.CodeOf(err) != errors.CodeNotFound || !in.AllowMissing {
      return nil, err
    }
    user = in.User
    user.CreateTime = now
  } else {
    if err := fieldmask.Update(in.UpdateMask, user, in.User); err != nil {
      // An error is thrown when an IMMUTABLE constraint is violated
      return nil, errors.Wrap(err, errors.CodeInvalidArgument, "invalid update mask")
    }
    // user.CreateTime is not updated because of the OUTPUT_ONLY constraint
  }
  user.UpdateTime = now
  if err := s.usersRepo.SaveUser(ctx, user); err != nil {
    return nil, err
  }
  return user, nil
}

As you can see, the new behavior of the fieldmask.Update function makes the code very easy to write and understand.

Hoping to see this implemented one day, I wish you a great day.

vallahaye avatar Jul 06 '22 19:07 vallahaye

Hey there, picking up on this thread.

Reading through the field behaviour AIP about immutable fields it says:

When a service receives an immutable field in an update request (or similar), even if included in the update mask, the service should ignore the field if the value matches, but should error with INVALID_ARGUMENT if a change is requested.

To me this means that the decision is left up to the service author to either ignore those fields or throw an error if a caller provides them.

With this in mind I'd like to add a new validation function to check if immutable fields are set during an update, similar to fieldbehavior.ValidateRequiredFieldsWithMask. This will allow service authors to validate the request and fail early but also not introduce a breaking change to the fieldmask.Update function.

What do you think?

DavyJ0nes avatar Oct 13 '22 16:10 DavyJ0nes

I put up a PR to implement the new validation function in #180

DavyJ0nes avatar Oct 13 '22 18:10 DavyJ0nes

This issue has been open for 365 days with no activity. Marking as stale.

github-actions[bot] avatar Apr 08 '24 08:04 github-actions[bot]