Repository navigation
Add clientWritable and paginate - #15
Merged
Merged
Conversation
Two features, both closing failures that run perfectly while being wrong. clientWritable: false on @ParseField. fromParams set every declared field the request carried - mass assignment. An endpoint accepting {"title"} also accepted {"title", "status": "published", "views": 9999}, and the row saved with values the caller was never meant to choose, silently. Marked on the model, the rule holds wherever fromParams is used instead of depending on every endpoint overwriting afterwards. The first implementation was fake, and the tests caught it: filtering inside the field loop came after Parse.Object.fromJSON had already assigned every key. The filter now runs on the source before the object is built. Discarding is silent to the CLIENT on purpose - refusing the request would tell an attacker which fields are protected, and would break a merely stale app. The developer is the one silence serves badly, so outside production a warning names the field, once per class and field. paginate(query, params, options). Every list endpoint hand-writes the same seven lines, and the obvious version returns results.length as the count - the size of the page the client already has, which nothing can paginate with. It shipped in this library's own starter. paginate reads limit and skip as the strings a GET sends, caps the limit, uses withCount for the true total, and computes hasMore. It also appends a createdAt tiebreaker to the sort. "Always sort" is not sufficient advice: sorting by a low-cardinality column leaves ties, and tied rows page exactly as badly as unsorted ones - repeated on two pages or dropped between them, with nothing reported. Appended, never overriding, so the endpoint still owns the order; tiebreak: false opts out for the caller whose indexes make the extra key expensive. The starter uses both: views is now server-owned, and listNotes shrinks to three lines of intent. RULES.md gains rule 7 and rewrites rule 6 from "overwrite after the call" to the declaration; the endpoint and model skills, CLAUDE.md, from-params.html and queries.html all follow, with search index entries. 356 tests pass, 21 of them new.
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.
Two features, both closing failures that run perfectly while being wrong.
clientWritable: false on @ParseField. fromParams set every declared field the request carried - mass assignment. An endpoint accepting {"title"} also accepted {"title", "status": "published", "views": 9999}, and the row saved with values the caller was never meant to choose, silently. Marked on the model, the rule holds wherever fromParams is used instead of depending on every endpoint overwriting afterwards.
The first implementation was fake, and the tests caught it: filtering inside the field loop came after Parse.Object.fromJSON had already assigned every key. The filter now runs on the source before the object is built.
Discarding is silent to the CLIENT on purpose - refusing the request would tell an attacker which fields are protected, and would break a merely stale app. The developer is the one silence serves badly, so outside production a warning names the field, once per class and field.
paginate(query, params, options). Every list endpoint hand-writes the same seven lines, and the obvious version returns results.length as the count - the size of the page the client already has, which nothing can paginate with. It shipped in this library's own starter. paginate reads limit and skip as the strings a GET sends, caps the limit, uses withCount for the true total, and computes hasMore.
It also appends a createdAt tiebreaker to the sort. "Always sort" is not sufficient advice: sorting by a low-cardinality column leaves ties, and tied rows page exactly as badly as unsorted ones - repeated on two pages or dropped between them, with nothing reported. Appended, never overriding, so the endpoint still owns the order; tiebreak: false opts out for the caller whose indexes make the extra key expensive.
The starter uses both: views is now server-owned, and listNotes shrinks to three lines of intent. RULES.md gains rule 7 and rewrites rule 6 from "overwrite after the call" to the declaration; the endpoint and model skills, CLAUDE.md, from-params.html and queries.html all follow, with search index entries.
356 tests pass, 21 of them new.