Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Required Fields V2 - Conditionally Required Fields #2099

Merged
merged 46 commits into from
Jan 29, 2025
Merged

Conversation

nick-Ag
Copy link
Member

@nick-Ag nick-Ag commented Jun 14, 2024

This PR revamps the required property for action input fields such that a destination builder can define conditionally required fields. These builder defined conditional requirements are then compiled into a JSONSchema4 object, which is used to validate each mapping before the perform block is triggered.

SDD: Link

Conditional Field Example:

For example, the Salesforce Lead last_name field is only required when a user is creating a lead. We can now encode that in the definition of the field directly like this:

    last_name: {
      label: 'Last Name',
      description: "The lead's last name. **This is required to create a lead.**",
      type: 'string',
      ...
      required: {
        conditions: [
          {
            fieldKey: 'operation',
            operator: 'is',
            value: 'create'
          }
        ]
      },

This builder defined condition will then be compiled into a JSONSchema like this:

{
  '$schema': 'http://json-schema.org/schema#',
  type: 'object',
  additionalProperties: false,
  properties: {
    operation: {
      title: 'Operation',
	...
    },
	...
    last_name: {
      title: 'Last Name',
	...
    },
	...
  },
  required: [ 'operation' ],
  if: {
    properties: {"operation": {"const": "create"}}
  },
  then: { "required": ["last_name"] }
}

And eventually will validate Payloads such as these:
{ operation: 'create', first_name: 'nick' } // invalid - missing last_name
{ operation: 'create', last_name: 'aguilar' } // valid - required last_name is included
{ operation: 'update', first_name: 'nick' } // valid - last_name is not required

Reviewing this PR:

~2800 lines added:

  • 1400 - schema-validation.test.ts.snap - this documents the JSON schema generated for each unit test. This is automated and viewing each of these generated schemas isn't important
  • 1100 - schema-validation.test.ts - New unit tests which run various combinations of different conditions a builder can define
  • 250 - fields-to-jsonschema.ts - The JSON Schema generation logic

Testing

Deployed successfully to staging. I have updated the Salesforce Lead action's last_name field with a conditional requirement.

As of now these conditions are not implemented in the UI, and I am only expecting new AJV error messages when the conditional requirement is not met.

The conditional requirement is enforced when operation: create but last_name is missing:
Screenshot 2024-12-02 at 10 19 17 AM

When operation: create and last_name is defined, the condition is met and event delivery succeeds:
Screenshot 2024-12-02 at 10 21 05 AM

When operation: update the condition is not enforced and last_name can be undefined:
Screenshot 2024-12-02 at 10 26 43 AM

  • Added unit tests for new functionality
  • Tested end-to-end using the local server
  • [Segmenters] Tested in the staging environment

Merge & Deploy Timing

This PR will be a part of a series of PRs. It should not be merged or deployed before these other PRs are ready.
@joe-ayoub-segment - I will handle the final deploy of this one

app: https://github.com/segmentio/app/pull/22618
action-cli: https://github.com/segmentio/action-cli/pull/196 💬

@joe-ayoub-segment
Copy link
Contributor

hi @nick-Ag - I know this is still in draft, so just out of curiosity what will this do?

  groupConditions: {
    anyOf: ['external_id', 'user_alias', 'email', 'braze_id']
  },

Will it hide or show fields in the UI depending on which fields have a mapping populated?
Or will it perform a runtime validation which will prevent the perform() function from being invoked if none of the fields have a value?

Keen to understand the intent here as I think the second approach would be really valuable.

@nick-Ag
Copy link
Member Author

nick-Ag commented Jun 20, 2024

hi @nick-Ag - I know this is still in draft, so just out of curiosity what will this do?

  groupConditions: {
    anyOf: ['external_id', 'user_alias', 'email', 'braze_id']
  },

Will it hide or show fields in the UI depending on which fields have a mapping populated? Or will it perform a runtime validation which will prevent the perform() function from being invoked if none of the fields have a value?

Keen to understand the intent here as I think the second approach would be really valuable.

Planning on the second approach primarily. I also want the validation to appear in the UX so that users can't save mappings where the group condition has not been met. Looking forward to showing more soon!

Copy link
Contributor

github-actions bot commented Nov 14, 2024

New required fields detected

Warning

Your PR adds new required fields to an existing destination. Adding new required settings/mappings for a destination already in production requires updating existing customer destination configuration. Ignore this warning if this PR is for a new destination with no active customers in production.

The following required fields were added in this PR:

  • Destination: Salesforce (Actions), Action Field(s):last_name

Add these new fields as optional instead and assume default values in perform or performBatch block.

@joe-ayoub-segment
Copy link
Contributor

Hi @nick-Ag , qq:

  • does/should the generated-types.ts file change if conditions are used?
  • Can you show examples how to use multiple conditions, with all/any (assuming those use-cases are supported)?

@nick-Ag
Copy link
Member Author

nick-Ag commented Dec 3, 2024

Hi @nick-Ag , qq:

  • does/should the generated-types.ts file change if conditions are used?
  • Can you show examples how to use multiple conditions, with all/any (assuming those use-cases are supported)?
  • The generated-types file won't change, fields which are conditionally required will be optional (ex: last_name?: string). See here
  • Those cases are supported, see the unit test here for an example. I've tried to test every possible use-case I could think of in that test file

maryamsharif
maryamsharif previously approved these changes Dec 3, 2024
@nick-Ag
Copy link
Member Author

nick-Ag commented Dec 4, 2024

hi @nick-Ag I can't wait to start using this new feature :). Can you tell me what happens when null, '' and undefined values are passed in to a string field which is conditionally required? Also what if the field is defined as AllowNull: true?

I'm going to make a small update to the PR to handle null values as well, thanks for pointing out this case

Copy link
Contributor

@varadarajan-tw varadarajan-tw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic work! Great tests!
Left just one question about nulls.

@joe-ayoub-segment
Copy link
Contributor

hi @nick-Ag. Couple of questions about how this code will behave:

  1. what will happen in the following scenario? I assume the UI will not fail validation. However when the event gets sent for processing it will fail? Does this mean that validation on the front end needs to be slightly different to validation that happens when an event is being processed?
{
   ... 
   label: 'last_name'
   conditions: [
          {
            fieldKey: 'operation',
            operator: 'is',
            value: 'create'
          }
    ]
 },
{
   ... 
   label: 'operation'
   default: {
      '@path': '$.propertries.operation'
   }
 }
  1. what will the error messages be when validation fails? How useful will they be? Are there any examples you can share? The reason I ask is that JSON schema error messages aren't always that helpful for the end user.

varadarajan-tw
varadarajan-tw previously approved these changes Jan 13, 2025
Copy link
Contributor

@varadarajan-tw varadarajan-tw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for answering my questions! Nice work! ✨ This will be very useful feature saving us some lines of code and bring in some consistency!

@pooyaj
Copy link
Contributor

pooyaj commented Jan 13, 2025

Looks great 🙌 This is not part of the scope? right?

    address: {
      label: 'Address',
      type: 'object',
      properties: {
        city: {
          label: 'city',
          type: 'string',
          required: [Condition Here]
        },

wondering if we handle it gracefully if someone puts a condition on the required field ( ideal if we can prevent it via typescript )

@nick-Ag
Copy link
Member Author

nick-Ag commented Jan 13, 2025

hi @nick-Ag. Couple of questions about how this code will behave:

  1. what will happen in the following scenario? I assume the UI will not fail validation. However when the event gets sent for processing it will fail? Does this mean that validation on the front end needs to be slightly different to validation that happens when an event is being processed?
{
   ... 
   label: 'last_name'
   conditions: [
          {
            fieldKey: 'operation',
            operator: 'is',
            value: 'create'
          }
    ]
 },
{
   ... 
   label: 'operation'
   default: {
      '@path': '$.propertries.operation'
   }
 }
  1. what will the error messages be when validation fails? How useful will they be? Are there any examples you can share? The reason I ask is that JSON schema error messages aren't always that helpful for the end user.
  1. Really good question here. In this case the UI will consider that a valid mapping and pass validation. However if there is no value at $.properties.operation for the event then the validation will fail at the actions level. The validation between the UI and event delivery is only different in the sense that UI validation checks only the users mapping (before path directives are applied) and the event delivery validation will validate against the final payload (path directives applied to the event).
  2. Here's an example of an error message in the case that last_name is missing.
[
	{
		"statusCode": 500,
		"message": "The root value is missing the required field 'last_name'. The root value must match \"then\" schema.",
		"stack": [
			"AggregateAjvError: The root value is missing the required field 'last_name'. The root value must match \"then\" schema.",
...

@nick-Ag
Copy link
Member Author

nick-Ag commented Jan 13, 2025

Looks great 🙌 This is not part of the scope? right?

    address: {
      label: 'Address',
      type: 'object',
      properties: {
        city: {
          label: 'city',
          type: 'string',
          required: [Condition Here]
        },

wondering if we handle it gracefully if someone puts a condition on the required field ( ideal if we can prevent it via typescript )

Fantastic question. Based on a unit test I just wrote that case is not handled correctly. Going to circle back and address it before merging. Will consider both preventing it entirely vs. implementing it correctly

@joe-ayoub-segment
Copy link
Contributor

Looks great 🙌 This is not part of the scope? right?

    address: {
      label: 'Address',
      type: 'object',
      properties: {
        city: {
          label: 'city',
          type: 'string',
          required: [Condition Here]
        },

wondering if we handle it gracefully if someone puts a condition on the required field ( ideal if we can prevent it via typescript )

Fantastic question. Based on a unit test I just wrote that case is not handled correctly. Going to circle back and address it before merging. Will consider both preventing it entirely vs. implementing it correctly

Nice question @pooyaj . My vote is to implement it if possible (if @nick-Ag can do it!). The reason is that Object fields are a neat way to squeeze a lot of fields into a small amount of page real estate. and they also allow us to locate related fields close together. It would be a missed opportunity if 'required' didn't work on these sub fields.

@nick-Ag
Copy link
Member Author

nick-Ag commented Jan 16, 2025

Looks great 🙌 This is not part of the scope? right?

    address: {
      label: 'Address',
      type: 'object',
      properties: {
        city: {
          label: 'city',
          type: 'string',
          required: [Condition Here]
        },

wondering if we handle it gracefully if someone puts a condition on the required field ( ideal if we can prevent it via typescript )

Fantastic question. Based on a unit test I just wrote that case is not handled correctly. Going to circle back and address it before merging. Will consider both preventing it entirely vs. implementing it correctly

Nice question @pooyaj . My vote is to implement it if possible (if @nick-Ag can do it!). The reason is that Object fields are a neat way to squeeze a lot of fields into a small amount of page real estate. and they also allow us to locate related fields close together. It would be a missed opportunity if 'required' didn't work on these sub fields.

Have implemented that use case & updated the README with some documentation about this feature. This one is ready to go out. Looking for final review @joe-ayoub-segment , @varadarajan-tw , @pooyaj

Comment on lines +376 to +389
creationName: {
label: "The name of the resource to create, required when operation = 'create'",
required: {
// This field is required only when the 'operation' field has the value 'create'
match: 'all',
conditions: [
{
fieldKey: 'operation',
operator: 'is',
value: 'create'
}
]
}
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @nick-Ag .
Can we include an example with dot notation for a reference to an object.property field?

Copy link
Contributor

@joe-ayoub-segment joe-ayoub-segment left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the functionality with @nick-Ag . I'm not the best person to review the code though, so please have someone who understands this part of the codebase do a review.

@nick-Ag nick-Ag merged commit 6be90dc into main Jan 29, 2025
14 checks passed
@nick-Ag nick-Ag deleted the required-fields-v2 branch January 29, 2025 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants