Skip to content

Conversation

zendesk-punavwalke
Copy link
Contributor

@zendesk-punavwalke zendesk-punavwalke commented Aug 11, 2025

💐

/cc @zendesk/wattle

Description

The PR implements validation limits on Custom Objects V2 requirements in ZAS

Custom Objects V2 requirements = Custom Objects v2 + custom object fields +custom object triggers

Requirement structure with required fields:

{
  "custom_objects_v2": {
    "objects": [
      {
        "key": "apartment",
        "title": "Apartment",
        "title_pluralized": "Apartments",
        "include_in_list_view": true
      }
    ],
    "object_fields": [
      {
        "key": "price",
        "title": "Price",
        "type": "decimal",
        "object_key": "apartment"
      }
    ],
    "object_triggers": [
      {
        "title": "Welcome Trigger",
        "object_key": "apartment",
        "conditions": {
          "all": [],
          "any": [
            {"field": "priority", "operator": "is", "value": "urgent"},
            {"field": "type", "operator": "is", "value": "incident"}
          ]
        },
        "actions": [
          {"field": "assignee_id", "value": "123456789"}
        ]
      }
    ]
  }
}

New Changes Introduced

  • Added new resource type custom_objects_v2 as a valid type in requirement.json
  • Added new validation module: CustomObjectsV2
    1. Purpose: Validates limits and schema requirements for Custom Objects V2
    2. Hard limits are enforced based on existing account settings limits & product requirements
    3. Validates excessive limits in custom_objects_v2 requirements and schema for custom objects, fields & triggers

Validation Limits Enforced

Object Limits

  • Maximum 50 custom objects per account
  • Maximum 10 fields per custom object
  • Maximum 20 triggers per custom object

Field Limits

  • Maximum 5 dropdown fields per object
  • Maximum 5 multiselect fields per object
  • Maximum 10 options per dropdown/multiselect field
  • Maximum 20 conditions in relationship filters

Trigger Limits

  • Maximum 50 conditions per trigger (combined all + any)
  • Maximum 25 actions per trigger

Payload Size Limits

  • Maximum 1MB total payload size

Validation Flow

  1. Structural validation - Ensures proper JSON structure
  2. Payload size validation - Enforces 1MB limit (early return if exceeded)
  3. Limits validation - Enforces count-based limits
  4. Schema validation - Validates required fields and structure

References

JIRA

RFC

Before merging this PR

  • Fill out the Risks section
  • Think about performance and security issues

Risks

  • [RUNTIME] Can this change affect apps rendering for a user?
    No, it cannot affect apps rendering for user because code changes in this PR are not yet used for app validations. We'll uncomment the once translations for validation errors are available.
  • [HIGH | medium | low] What features does this touch?
    medium: Right now, it does not touch any existing feature. We are introducing a new resource_type which is not yet used in ZAM or any of the existing apps.

@zendesk-punavwalke zendesk-punavwalke force-pushed the zendesk-punavwalke/CRYSTAL-368-enforce-cov2-limits branch from d8fb91b to 5516c54 Compare August 16, 2025 12:43
@zendesk-punavwalke zendesk-punavwalke changed the title enforce cov2 limits in req validation [CRYSTAL-368]Enforce cov2 limits in requirment validations Aug 16, 2025
@zendesk-punavwalke zendesk-punavwalke marked this pull request as ready for review August 18, 2025 06:59
@zendesk-punavwalke zendesk-punavwalke requested a review from a team as a code owner August 18, 2025 06:59
class << self
def call(requirements)
custom_objects_v2_requirements = requirements[AppRequirement::CUSTOM_OBJECTS_V2_KEY]
return [] unless custom_objects_v2_requirements

Choose a reason for hiding this comment

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

If I'm not mistaken, custom_objects_v2_requirements contains a hash value. If we get {}, then it won’t serve the intended purpose because an empty hash evaluates to true in a conditional statement. It would be better to use the .present? method with unless, or the .blank? method with if to handle the condition correctly.

objects = custom_objects_v2_requirements['objects']
return [] unless objects

return [] unless objects.size > MAX_OBJECTS

Choose a reason for hiding this comment

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

How about replacing the unless condition with an if statement that returns the error? This change would make the code more readable.

# ========== OBJECTS VALIDATION ==========

def validate_objects_excessive_limit(custom_objects_v2_requirements)
objects = custom_objects_v2_requirements['objects']

Choose a reason for hiding this comment

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

Could we use constant for the keys? (ex: 'objects')

Copy link
Contributor

Choose a reason for hiding this comment

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

Why would a constant be better in this case? Curious to know more.

Choose a reason for hiding this comment

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

Keys play a crucial role in validation and they are fixed. Using constants ensures consistency wherever the keys are used. Additionally, if the keys need to be updated, making changes in one place is much easier and we will have a set of keys that are important for validations.

@digesh-parecha
Copy link

I haven't added these comments everywhere; they apply to every place where the scenario matches.
#383 (comment)
#383 (comment)

@digesh-parecha
Copy link

Could you please include the final requirements structure in the description including all the required keys? It would be very helpful during the review process.

Copy link
Contributor

@mmassaki mmassaki left a comment

Choose a reason for hiding this comment

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

Nice work! I'm still reviewing the PR and should be adding more comments later.

Comment on lines 83 to 87
def validate_custom_objects_v2_requirements(requirements) # rubocop:disable Lint/UnusedMethodArgument
# TODO: Uncomment this code
# when translations for validation errors are avilable to use in CustomObjectsV2 module
# CustomObjectsV2.call(requirements)
[]
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this code be updated and this PR only merged once the translations are available?

I'd recommend avoiding pushing commented out code and TODO comments for later as best practice.

Comment on lines 29 to 30
def call(requirements)
custom_objects_v2_requirements = requirements[AppRequirement::CUSTOM_OBJECTS_V2_KEY]
Copy link
Contributor

Choose a reason for hiding this comment

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

Considering SoC, how about receiving only COv2 requirements directly in this class since it doesn't need all the app requirements?

This would allow custom_objects_v2_requirements to be renamed to simply requirements as this class is scoped to COv2 requirements.

# lib/zendesk_apps_support/validations/requirements.rb
def validate_custom_objects_v2_requirements(requirements)
  CustomObjectsV2.call(requirements[AppRequirement::CUSTOM_OBJECTS_V2_KEY])
end

class << self
def call(requirements)
custom_objects_v2_requirements = requirements[AppRequirement::CUSTOM_OBJECTS_V2_KEY]
return [] unless custom_objects_v2_requirements
Copy link
Contributor

Choose a reason for hiding this comment

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

How about setting a default value for custom_objects_v2_requirements to avoid this conditional?

Comment on lines 59 to 62
def validate_objects_excessive_limit(custom_objects_v2_requirements)
objects = custom_objects_v2_requirements['objects']
return [] unless objects

Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to the comments above. How about the following to simplify it?

Suggested change
def validate_objects_excessive_limit(custom_objects_v2_requirements)
objects = custom_objects_v2_requirements['objects']
return [] unless objects
def validate_objects_excessive_limit(objects = [])

Then it can be called like that

validate_objects_excessive_limit(custom_objects_v2_requirements['objects'])

# or just `requirements` after renaming `custom_objects_v2_requirements` to `requirements`.
validate_objects_excessive_limit(requirements['objects'])

As there seems to be a pattern in this class, this applies to the other methods as well.

Comment on lines 136 to 141
value: "The requirements.json file contains too many custom object fields of type dropdown. The current limit is %{max} fields per object. This app has %{count} fields of type dropdown for object %{object_key}."
screenshot: "https://drive.google.com/file/d/1hILvdZQXZCtkQVupYXedPbnD_rNN4nwH/view?usp=sharing"
- translation:
key: "txt.apps.admin.error.app_build.excessive_cov2_multiselect_fields_per_object"
title: "App builder job: requirements file contains too many custom object fields of type multiselect. The maximum number of fields of type multiselect per object is 5. Leave requirements.json as is (do not translate)"
value: "The requirements.json file contains too many custom object fields of type multiselect. The current limit is %{max} fields per object. This app has %{count} fields of type multiselect for object %{object_key}."
Copy link
Contributor

Choose a reason for hiding this comment

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

Should dropdown and multiselect be a placeholder for these translations since they are keys in the schema? I guess we could use the same translation for both validations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

dropdown and multiselect are indeed schema keys but I did check in acf custom fields, they were translated. Hence I decided to have two separate translations for both. Still I am open to suggestions!

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it can be confusing if they are translated in the error message given the value used in requirements.json is not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed! Will change it to use a single translation 👍

Comment on lines 103 to 105
error = field_type == 'dropdown' ? # rubocop:disable Style/MultilineTernaryOperator
:excessive_cov2_dropdown_fields_per_object :
:excessive_cov2_multiselect_fields_per_object
Copy link
Contributor

Choose a reason for hiding this comment

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

This was a code smell for me that made me review the translations. This condition might not be needed when using the same translation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you are right. I did stated here the reason for two translations and hence this condition.

@zendesk-punavwalke
Copy link
Contributor Author

@digesh-parecha @mmassaki Added final requirements structure in the description including all the required keys. And I did found the missing validations for required keys in triggers, will add those in next commit.

Comment on lines 91 to 93
SELECTION_FIELD_LIMITS.map do |field_type, max_limit|
validate_field_type_limit(object_fields, field_type, max_limit)
end.flatten
Copy link
Contributor

Choose a reason for hiding this comment

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

Would #flat_map work here?

This applies to other parts of the code calling #map#flatten

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes it would work here 👍, Thanks for calling it out!

:excessive_cov2_dropdown_fields_per_object :
:excessive_cov2_multiselect_fields_per_object

check_collection_limits(fields_by_object, max_limit, error, field_type: field_type)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is field_type being used in #check_collection_limits?

Copy link
Contributor Author

@zendesk-punavwalke zendesk-punavwalke Aug 20, 2025

Choose a reason for hiding this comment

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

yes, in method check_collection_limits it a common helper method which has 4 standard parameters and uses the **context splat operator to accept any addition params.
You can check here

Comment on lines 120 to 131
[].tap do |errors|
fields_with_options.each do |field|
options = field['custom_field_options']
next if options.size <= max_limit

errors << ValidationError.new(:excessive_cov2_field_options,
max: max_limit,
count: options.size,
field_key: field['key'],
object_key: field['object_key'])
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be replaced with #filter_map to simplify?

Suggested change
[].tap do |errors|
fields_with_options.each do |field|
options = field['custom_field_options']
next if options.size <= max_limit
errors << ValidationError.new(:excessive_cov2_field_options,
max: max_limit,
count: options.size,
field_key: field['key'],
object_key: field['object_key'])
end
end
fields_with_options.filter_map do |field|
options = field['custom_field_options']
if options.size > max_limit
ValidationError.new(:excessive_cov2_field_options,
max: max_limit,
count: options.size,
field_key: field['key'],
object_key: field['object_key'])
end

[].tap do |errors|
fields_with_options.each do |field|
options = field['custom_field_options']
next if options.size <= max_limit
Copy link
Contributor

Choose a reason for hiding this comment

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

Could options be nil here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes it can be nil, and can throw the validations error which exceeding limit. Thanks for pointing this out.

Comment on lines 201 to 213
[].tap do |errors|
triggers.each do |trigger|
next unless trigger['actions']

actions = trigger['actions']
next if actions.size <= MAX_ACTIONS_PER_TRIGGER

errors << ValidationError.new(:excessive_custom_objects_v2_trigger_actions,
max: MAX_ACTIONS_PER_TRIGGER,
count: actions.size,
trigger_title: trigger['title'])
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above. How about using #filter_map?

Comment on lines 297 to 307
[].tap do |errors|
grouped_items.each do |object_key, items|
next unless items.size > max_limit

errors << ValidationError.new(error,
max: max_limit,
count: items.size,
object_key: object_key,
**context)
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above. How about using #filter_map?

Comment on lines 301 to 305
errors << ValidationError.new(error,
max: max_limit,
count: items.size,
object_key: object_key,
**context)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could object_key be nil here when the object_key is missing in fields and triggers? If so, should this be excluded from this validation given they should be flagged by the require key validation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, object_key can be nil when it is not defined. It will be flagged by schema validation, hence can be skipped here. Thanks for pointing it out!

%w[all any].sum { |key| conditions[key]&.size || 0 }
end

def check_collection_limits(grouped_items, max_limit, error, **context)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this method be called validate_colection_limits to be consistent with the other methods in this class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it should be, thanks for pointing it out 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants