Skip to content

fix: stabilize TS field order for :auto calcs with literal map exprs#67

Closed
barnabasJ wants to merge 1 commit intoash-project:mainfrom
barnabasJ:fix/auto-calc-field-order
Closed

fix: stabilize TS field order for :auto calcs with literal map exprs#67
barnabasJ wants to merge 1 commit intoash-project:mainfrom
barnabasJ:fix/auto-calc-field-order

Conversation

@barnabasJ
Copy link
Copy Markdown
Contributor

Summary

  • :auto-typed Ash calculations with literal map expressions (e.g. calculate :summary, :auto, expr(%{a: id, b: name})) emit TypeScript interfaces whose field order changes non-deterministically between compiles. Ash materializes the literal map through a runtime Erlang map whose iteration order depends on atom term ordering at BEAM load time — so warm _build/dev and clean _build/test can disagree. This manifests as flaky snapshot diffs and unstable generated TS.
  • Fix is local to ash_typescript: sort :fields alphabetically at the calc- and publication-introspection sites known to carry auto-derived constraints. User-declared typed maps (attribute :foo, :map, constraints: [fields: [...]]) pass through unchanged — their source order is preserved by the DSL and was already deterministic.

Changes

  • lib/ash_typescript/codegen/helpers.ex — Adds sort_auto_fields/1 and auto_safe_calc_constraints/1. The latter only sorts when the calc is expression-based (calc.calculation = {Ash.Resource.Calculation.Expression, _}) — the sole source of non-determinism.

  • lib/ash_typescript/codegen/type_mapper.ex — Wraps calc.constraints through the helper in the %Ash.Resource.Calculation{} schema-emission branch.

  • lib/ash_typescript/codegen/resource_schemas.ex — Same treatment in get_calculation_return_type_for_metadata/2.

  • lib/ash_typescript/typed_channel/codegen.ex — When a pub's transform: is a calc-name atom (meaning Ash derived returns/constraints from a calc), sort fields via sort_auto_fields/1 before passing to map_channel_payload_type/2.

  • Tests — New test/ash_typescript/codegen/auto_calc_field_order_test.exs covers:

    • Unit tests documenting the helper contracts.
    • A byte-identical property test that feeds three shuffled synthetic :fields orderings through auto_safe_calc_constraints/1 + map_type/3 and asserts identical output.
    • An end-to-end typed-channel codegen test asserting TrackerOrderedCardPayload emits alphabetically sorted fields.

    Caveat on detection power: because the non-determinism lives upstream in Ash's expression-to-type resolver, we can't force a reliably non-alphabetical input from a test. These tests verify the fix's contract and post-fix behavior; they don't guarantee to fail on every pre-fix BEAM load for every input.

    Three existing assertions in typed_channel/codegen_test.exs locked in source order for TrackerDetailPayload, TrackerDeepDetailPayload, and TrackerReportPayload and have been updated to the new alphabetical order.

Why the fix lives here

The upstream root cause is in Ash's determine_type path — resolving a map literal expr into {Ash.Type.Map, fields: [...]}. Fixing it there would require either a new expression-tree struct (propagates to ash_postgres and every other data layer) or a side-channel for ordering through the DSL pipeline. Neither is worth it to give one consumer (codegen) a stable input. Since TypeScript field order is purely cosmetic, alphabetizing at the ash_typescript consumer is the right layer.

Test plan

  • mix test — 2226 tests, 0 failures
  • mix format --check-formatted clean
  • mix credo --strict clean
  • User-declared typed-map field ordering preserved (auto_safe_calc_constraints/1 only sorts for expression-based calcs)

Ash resolves `expr(%{a: ..., b: ...})` in `:auto`-typed calcs through a
runtime Erlang map, whose iteration order depends on atom term ordering
at BEAM load time (warm `_build/dev` vs clean `_build/test` can differ).
This leaked into generated TypeScript as non-deterministic field order.

Fix is local to ash_typescript: sort the `:fields` constraint
alphabetically at the calc- and publication-introspection sites known to
carry auto-derived constraints. User-declared typed maps are unaffected
(their source order is preserved by the DSL).
Copy link
Copy Markdown
Contributor

@zachdaniel zachdaniel left a comment

Choose a reason for hiding this comment

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

I think that we could cut down the explanatory comment though.

@barnabasJ barnabasJ closed this Apr 18, 2026
@barnabasJ
Copy link
Copy Markdown
Contributor Author

I think that we could cut down the explanatory comment though.

The fix in Ash should be enough, I thought I stopped this work before the PR was opened.

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.

2 participants