Changelog
View SourceAll notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Changed
- Breaking. A read action carrying
identityis rejected withidentity_not_supportedinstead of having the parameter dropped (#44).identityselects a record for update and destroy; reads select one withget_by, the same split upstreamash_typescriptdraws. A read used to build no filter at all, so the caller named one record and got the whole table, or aMultipleResultsfromAsh.read_one/1. Generated Swift clients fromash_kotlin_multiplatformsendidentityonget?reads and must move those calls toget_by. - Breaking. A
nullidentity value is rejected withinvalid_identity(#44). It used to compile tokey == nil, which Ash evaluates as unknown, so the lookup matched nothing and surfaced asNotFound.
Fixed
- A failing bulk action reaches the client as one error per failed field
(#16).
Ash.bulk_create/update/destroyreturn%Ash.BulkResult{errors: [...]}and the pipeline forwards that list. A list matched neitheris_exceptionnoris_mapinbuild_error_response/1, so every per-record validation error collapsed into one "An unexpected error occurred". - A Reactor-backed action reports the error its step produced, not the
wrapper (#17).
%Reactor.Error.Invalid.RunStepError{}is itself an exception, so it matched the generic Ash clause and the client got a step-execution notice naming a step it has never heard of. Ash.Type.Vectorvalues serialize as a list of numbers (#17).%Ash.Vector{}keeps its floats in a packed binary thatJasonrefuses to encode, so selecting a vector field either crashed the encoder or put%{data: <<...>>, dimensions: n}on the wire.
[0.3.0] - 2026-09-09
Two breaking changes to the RPC error payload. Run the codemod that ships with this release before anything else:
mix igniter.upgrade ash_introspectionIt rewrites Elixir code reading code off an error to type, and prints the
shapes it could not decide — pattern matches such as %{code: code} = error,
reads off an expression rather than a variable, and generated TypeScript or
Kotlin clients, which you regenerate instead. To run it on its own:
mix ash_introspection.upgrade 0.2.0 0.3.0.
Security
Forbidden errors no longer render the policy breakdown to the client (#11).
Exception.message/1onAsh.Error.Forbidden.Policyreturns the whole authorization report — every policy, every check outcome, and the actor inspected in full — whenever Ash's app-wideconfig :ash, :policies, show_policy_breakdowns?: trueis set, so a development toggle opened every RPC response. The message is now the static"forbidden"and thepolicy_breakdownkey is gone.Unknown errors no longer return the raw exception text (#11).
Ash.Error.Unknown.UnknownErroris the bucket every unrecognised exception falls into, so its message could be a connection string, a stack trace or a third-party library's internals. Clients now get"Something went wrong".Client-supplied field names no longer mint atoms (#10). The atom table is never garbage collected, so a request carrying unknown field names grew it without bound and could take the node down. Field selection now resolves names against atoms that already exist and rejects the rest as unknown fields.
Changed
Breaking: every RPC error names its class under
type(#14). TheAshIntrospection.Rpc.Errorprotocol already emittedtype, but the fallback paths inAshIntrospection.Rpc.Errorsemittedcode, so a client readingerror.typegotnilfor exactly the errors it could not anticipate — an exception with no protocol implementation, an implementation that raised, and a domain withshow_raised_errors?set. Thecodekey is gone. A client readstype.Breaking: an error's
%{name}placeholders now match the keys invars(#14). An error map is a template:message,short_messageand the strings underdetailscarry placeholders,varscarries the values, and the client interpolates so it can localize. Stage 4 camelized every nested key,varsincluded, and never touched the message, so%{action_name}was left naming a key that had becomeactionNameand interpolation silently produced nothing. Ten multi-word placeholders were affected:%{action_name},%{allowed_fields},%{error_type},%{expected_keys},%{expected_members},%{extra_fields},%{field_type},%{found_keys},%{provided_keys}and%{return_type}. A client that hardcodes a snake_case placeholder name must now read the key fromvars.
Added
mix ash_introspection.upgrade(#34) — the Igniter task carrying a codemod per breaking release, somix igniter.upgrade ash_introspectionmigrates consumer code instead of leaving it to a hand search.AshIntrospection.ErrorFormatter.format/2— formats an error's keys and rewrites its placeholders in one pass, which is the only way the two can stay consistent.AshIntrospection.FieldFormatter.format_output_field_names/2— the recursive key walk the RPC pipeline used privately, now shared so error payloads and the response envelope agree.config :ash_introspection, :policies, show_policy_breakdowns?: true— opt in to sending the policy breakdown back as the forbidden error's message. Deliberately a separate setting from Ash's own:ash, :policies, so an app-wide development toggle cannot open RPC responses in production.AshIntrospection.FieldFormatter.resolve_field_name/2— resolves a field name to an existing atom, or returns the formatted string when none exists.AshIntrospection.Rpc.FieldProcessing.Validation.field_exists?/2— field existence check that accepts a string name, whichKeyword.has_key?/2rejects.
Fixed
- String field names on a resource with no interop name mapping resolved to
nil, so valid names were rejected and unknown ones reported as{:unknown_field, nil, resource, path}.
Removed
- Breaking:
AshIntrospection.FieldFormatter.convert_to_field_atom/2. Its contract was to always produce an atom, which is the vulnerability itself. Callers passing client input should useresolve_field_name/2; callers that genuinely hold a trusted name already have the atom.
[0.2.0] - 2025-12-21
Added
- Action introspection module (
AshIntrospection.Codegen.ActionIntrospection)- Pagination support detection (offset/keyset)
- Input type analysis (required/optional/none)
- Return type analysis for generic actions
- Validation error types module
(
AshIntrospection.Codegen.ValidationErrorTypes)- Error type classification for code generation
- Action input error classification
- Comprehensive documentation in README
Changed
- Enhanced type discovery with better cycle detection
- Improved field selector validation
[0.1.0] - 2025-11-25
Added
- Initial release
- Core type system introspection (
AshIntrospection.TypeSystem.Introspection)- Type classification and NewType unwrapping
- Union type extraction
- Field name callback detection
- Resource fields lookup (
AshIntrospection.TypeSystem.ResourceFields) - RPC pipeline for action execution (
AshIntrospection.Rpc.Pipeline)- 4-stage execution: parse, execute, process, format
- Request structure (
AshIntrospection.Rpc.Request) - Value formatter for bidirectional type conversion
- Result processor with extraction templates
- Field processing modules
- Atomizer for field name conversion
- Field selector for recursive selection
- Validation for duplicate detection
- Error handling modules
- Error protocol for exception extraction
- Error builder for message generation
- Central error processing pipeline
- Type discovery for code generation (
AshIntrospection.Codegen.TypeDiscovery) - Field formatter utilities (camelCase, PascalCase, snake_case)