Lightning.Validators (Lightning v2.19.0-pre)

View Source

Extra validators for Ecto.Changeset.

Summary

Functions

True when a string is made up entirely of characters that draw nothing.

Folds a name to the form used when matching it against a stored one.

Returns true when value is a well-formed UUID that will dump cleanly to a :binary_id on insert/update.

Validates that an email field contains a properly formatted email address.

Validate that only one of the fields is set at a time.

Normalises a name field and rejects any control character in it.

Rejects a name that will not fit the column it is stored in.

Rejects a NUL in a field that ends up inside a jsonb column.

Rejects a NUL anywhere inside a map field that ends up in a jsonb column.

Validate that at least one of the fields is set.

Validates a URL in a changeset field.

Validates that the given field(s) contain a well-formed UUID.

Functions

invisible_only?(value)

@spec invisible_only?(binary()) :: boolean()

True when a string is made up entirely of characters that draw nothing.

Exposed so the fixture that keeps the client's copy of this rule in step can be generated from it. See assets/js/utils/nameValidation.ts.

normalize_name_for_match(value)

@spec normalize_name_for_match(term()) :: term()

Folds a name to the form used when matching it against a stored one.

Only NFC and a trim, which is what validate_name/3 applies on save, so a caller comparing a name that never went through a changeset against one that did is comparing like with like. Not case folding: names are case-sensitive.

valid_uuid?(value)

@spec valid_uuid?(term()) :: boolean()

Returns true when value is a well-formed UUID that will dump cleanly to a :binary_id on insert/update.

Uses Ecto.UUID.dump/1 (not cast/1): dump rejects raw 16-byte binaries and unsubstituted import placeholders that cast would accept, matching what the database actually enforces. nil is not a valid UUID.

This is the single source of truth for the "dumpable UUID" check — both validate_uuid/2 and schema-level guards (e.g. Workflows.Job) build on it so they cannot drift apart.

validate_email_format(changeset, field \\ :email)

@spec validate_email_format(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()

Validates that an email field contains a properly formatted email address.

Applies: required check, format regex, max 160 chars, lowercases the value. This is a pure format check — no database lookup. Use User.validate_email/1 when you also need to verify the email is unique in the users table.

validate_exclusive(changeset, fields, message)

@spec validate_exclusive(Ecto.Changeset.t(), [atom()], String.t()) ::
  Ecto.Changeset.t()

Validate that only one of the fields is set at a time.

Example:

changeset
|> validate_exclusive(
  [:source_job_id, :source_trigger_id],
  "source_job_id and source_trigger_id are mutually exclusive"
)

validate_name(changeset, field, message \\ "can't contain control characters")

@spec validate_name(Ecto.Changeset.t(), atom(), String.t()) :: Ecto.Changeset.t()

Normalises a name field and rejects any control character in it.

The value is normalised to NFC and trimmed before anything else runs, so a later validate_required/3 or validate_length/3 in the same changeset sees the value that will actually be stored. Call this straight after cast/3.

assets/js/utils/nameValidation.ts is the client-side copy of the rule.

validate_name_fits_column(changeset, field, message, width \\ 255)

@spec validate_name_fits_column(
  Ecto.Changeset.t(),
  atom(),
  String.t(),
  pos_integer()
) :: Ecto.Changeset.t()

Rejects a name that will not fit the column it is stored in.

Postgres counts a varchar in codepoints; the product caps above this one count graphemes, so a name built from multi-codepoint clusters can pass a 100 grapheme cap and raise 22001 on insert.

Skipped when the field already has an error, so a plainly over-long name gets the product cap's message and this one stays quiet. width defaults to 255, the width of every name column in this schema; pass it for a narrower one such as credentials.schema.

The message callers pass should not quote a number: from where the user sits the limit is the product cap.

validate_no_null_bytes(changeset, field, message)

@spec validate_no_null_bytes(Ecto.Changeset.t(), atom(), String.t()) ::
  Ecto.Changeset.t()

Rejects a NUL in a field that ends up inside a jsonb column.

Postgres refuses a NUL anywhere inside a jsonb value (22P05). Unlike a name, these fields legitimately hold newlines and tabs, so only the NUL is refused rather than the whole control set. Malformed UTF-8 goes the same way.

validate_no_null_bytes_deep(changeset, field, message)

@spec validate_no_null_bytes_deep(Ecto.Changeset.t(), atom(), String.t()) ::
  Ecto.Changeset.t()

Rejects a NUL anywhere inside a map field that ends up in a jsonb column.

Walks the whole structure rather than checking the top level, because a NUL in a key is just as fatal as one in a value.

validate_one_required(changeset, fields, message)

@spec validate_one_required(Ecto.Changeset.t(), [atom()], String.t()) ::
  Ecto.Changeset.t()

Validate that at least one of the fields is set.

validate_required_assoc(changeset, assoc, message \\ "is required")

@spec validate_required_assoc(Ecto.Changeset.t(), atom(), String.t()) ::
  Ecto.Changeset.t()

Validate that an association is present

NOTE This should only be used when using put_assoc, not cast_assoc. cast_assoc provides a required: true option. Unlike validate_required, this does not add the field to the required list in the schema.

validate_url(changeset, field)

@spec validate_url(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()

Validates a URL in a changeset field.

Ensures that the URL:

  • Has a valid http or https scheme.
  • Has a valid host (domain name, IPv4, or IPv6).
  • The host is not blank and does not exceed 255 characters.

Returns a changeset error for invalid URLs.

validate_uuid(changeset, fields)

@spec validate_uuid(Ecto.Changeset.t(), atom() | [atom()]) :: Ecto.Changeset.t()

Validates that the given field(s) contain a well-formed UUID.

:binary_id fields are not format-checked by cast/3 — a malformed value (e.g. an unsubstituted import placeholder) passes casting and only raises Ecto.ChangeError when dumped on insert/update. This converts that into a changeset error instead.

Only runs when a non-nil change is present for the field, so optional foreign keys left unset are unaffected.

Narrowing: uses Ecto.UUID.dump/1, not cast/1. dump additionally rejects raw 16-byte binaries and unsubstituted placeholders that cast accepted. Confirmed no live caller relied on the laxer behaviour (uppercase canonical UUIDs still pass).

changeset
|> validate_uuid([:id, :workflow_id])