feat(database): expose 422 validation errors from Model.save()#109
Merged
Conversation
Model.save() returned only a bool and discarded the server's 422 error body, so callers could not surface per-field validation messages. Capture the Laravel-shape errors into validationErrors (with a validationError(field) convenience), cleared at the start of each save and populated on a non-successful response. The bool return is unchanged (backward compatible); a thrown transport error leaves the map empty so callers can distinguish a field-validation failure from a network failure.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR adds a small, additive validation-error surface to the Eloquent persistence layer so callers can read Laravel-style 422 field errors after Model.save() fails, enabling ORM-backed forms to render inline per-field messages.
Changes:
- Added
validationErrors(map) andvalidationError(field)helpers toInteractsWithPersistence. - Updated remote
save()flow to clear prior errors and populate them from a failed response viaMagicResponse.errors. - Added tests covering 422 population, clearing on subsequent success, and ensuring non-field failures (500) don’t populate field errors.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| lib/src/database/eloquent/concerns/interacts_with_persistence.dart | Adds validation error state + accessors and wires population/clearing into the remote save path. |
| test/database/eloquent/model_test.dart | Adds regression tests for 422 error extraction/clearing behavior on save(). |
… contract
Map.unmodifiable is shallow: MagicResponse.errors builds a fresh map of
growable lists, so a caller could still mutate the per-field messages the
getter handed out. _extractValidationErrors now freezes the map and every
list inside it, the field holds that frozen map (const {} when empty), and
the getter returns it directly instead of re-wrapping on each read.
The doc comment also claimed a successful save always leaves the map
empty. It does not: the map tracks the REMOTE leg, so a hybrid model whose
remote save 422s while its local write succeeds returns true with the
errors filled. Say so, and drop the '?.isNotEmpty == true' plus bang from
validationError() for a plain null-and-empty guard.
Adds an unmodifiable-at-both-levels test, the CHANGELOG entry the
contributing checklist asks for, and the doc + skill pages for the new
public members.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
InteractsWithPersistence.save()returned only abooland threw away the server's 422 response body, so a consumer could not show per-field validation errors after a failed save (only a generic "save failed").Change
Map<String, List<String>> validationErrors(unmodifiable getter) andString? validationError(String field)on the persistence mixin.save()clears them at the start of the remote block and populates them from a non-successful response (Laravel{message, errors: {field: [...]}}shape) viaMagicResponse.errors.Future<bool>return contract is unchanged; a thrown transport error leaves the map empty, so callers distinguish a field-validation failure (422) from a network/500 failure.Why
Enables ORM-backed forms to render server field errors inline (the uptizm monitoring forms need this). Backward compatible and additive.
Tests
test/database/eloquent/model_test.dart: a faked 422 populatesvalidationErrorsand a subsequent success clears them; a 500 leaves them empty. Fullflutter test test/databasegreen.