-
-
Notifications
You must be signed in to change notification settings - Fork 0
The Where vocabulary and its lowering; the filter module retyped #1220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| --- | ||
| '@opensaas/stack-core': minor | ||
| '@opensaas/stack-ui': minor | ||
| '@opensaas/stack-cli': patch | ||
| --- | ||
|
|
||
| The secured surface takes the closed Where vocabulary, lowered in one place | ||
|
|
||
| `context.db.<List>.where(...)` now accepts the whole vocabulary — `equals`, | ||
| `not`, `in`, `notIn`, `lt`, `lte`, `gt`, `gte`, `contains`, the `AND`/`OR`/`NOT` | ||
| combinators, and `some`/`every`/`none` on a relation of any cardinality — plus a | ||
| new scalar-only `.orderBy()`. Everything lowers onto the ORM's predicate lambda | ||
| in one place (ADR-0055). | ||
|
|
||
| ```typescript | ||
| const posts = await context.db.Post.where({ | ||
| OR: [{ title: { contains: 'release' } }, { views: { gte: 100 } }], | ||
| author: { some: { handle: { equals: 'ada' } } }, | ||
| }) | ||
| .orderBy({ views: 'desc' }) | ||
| .all() | ||
| ``` | ||
|
|
||
| - `contains` is engine-escaped and case-insensitive, so `contains: '50%'` matches | ||
| a literal per-cent sign rather than binding a wildcard. | ||
| - `equals: null` lowers to `IS NULL`, `not: null` to `IS NOT NULL`. | ||
| - A relation predicate scopes the `EXISTS` by the related list's own `query` | ||
| access. `some` and `none` ask about the rows the caller may see; `every` asks | ||
| whether every row the caller may see matches, so a row the caller cannot see | ||
| never decides the parent's membership. A related list the session cannot | ||
| query is the empty set: `some` is false, `none` and `every` are true. | ||
| - An Access Filter that scopes by a relation is expanded into the related list's | ||
| own Access Filter. A filter that expands into itself — directly, or through | ||
| another list — throws `AccessFilterRecursionError` naming the chain, rather | ||
| than recursing until the process runs out of memory. An acyclic chain deeper | ||
| than ten lists is refused the same way. Failing closed is deliberate: a | ||
| truncated Access Filter is a widened read. | ||
| - An unknown key or operator is a `ValidationError` naming the list and the key, | ||
| under `sudo` too. A key the session cannot read is refused with the identical | ||
| message a key the list does not declare gets, so the refusal is not an | ||
| existence oracle; a denied caller still gets the Silent failure first and sees | ||
| no validation error at all. | ||
|
|
||
| **Lowering is now total.** A condition that resolved to `undefined` is refused | ||
| rather than dropped, on both spellings. An access rule written as | ||
| `({ session }) => ({ authorId: session?.userId })` used to match every row for an | ||
| anonymous caller; it now throws. Spell the denial: | ||
|
|
||
| ```typescript | ||
| // Before — silently matched everything when session was null | ||
| query: ({ session }) => ({ authorId: session?.userId }) | ||
|
|
||
| // After | ||
| query: ({ session }) => (session ? { authorId: { equals: session.userId } } : false) | ||
| ``` | ||
|
|
||
| The same refusal now covers the clause `mergeFilters` folds in, so the guarantee | ||
| holds on every surface rather than only on `.where().all()/.first()`: an access | ||
| filter carrying an `undefined` condition anywhere (including nested under an | ||
| operator or inside an `AND`/`OR` branch) throws the new, exported | ||
| `UndefinedAccessFilterError`. A caller's own `where` is untouched — this applies | ||
| only to what an access rule returns. | ||
|
|
||
| **Also changed:** the filter engine's `FilterCondition` is a Where vocabulary | ||
| value; a to-one relationship's label filter emits `some` rather than `is`; a | ||
| to-many count filter shrinks to presence (`orders:0` → `none`, `orders:>0` / | ||
| `orders:>=1` → `some`, any other comparison degrades to free text); | ||
| and read-path key validation rejects an operator outside the vocabulary | ||
| (`startsWith`, `endsWith`, `mode`, `search` and the array/JSON operators are | ||
| gone). | ||
|
|
||
| **Removed exports.** These have no replacement — the behaviour they carried is | ||
| either gone or now expressed in the Where vocabulary: | ||
|
|
||
| | Removed | What to do instead | | ||
| | --------------------------------------------- | -------------------------------------------------------------- | | ||
| | `RELATIONSHIP_COUNT_FILTER_KEY` | Nothing — the count-filter marker no longer exists. | | ||
| | `RelationshipCountFilterMarker` | Nothing — same. | | ||
| | `resolveRelationshipCountFilters` | Nothing — a count filter shrinks to `some`/`none` when parsed. | | ||
| | `resolveRelationshipLabelFilters` | Nothing — a to-one label filter emits `some` directly. | | ||
| | `isToOneRelationshipField` | Read `many` off the relationship field config. | | ||
| | `ColumnEquality`, `UnsupportedPredicateError` | Gone with the predicate builder they belonged to. | | ||
|
|
||
| **Newly exported:** `UndefinedAccessFilterError` and, from the secured surface, | ||
| `AccessFilterRecursionError`. |
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
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
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
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"the engine refuses a predicate that resolved to
undefined" is only true of the secured surface — thefindManypath still fails open.The totality fix lives in
resolveWhere, which onlycontext.db.<List>.where(...).all()/.first()goes through.createFindMany(andcount/updateMany/delete) still doesmergeFilters(scopedWhere, accessResult), which just returns{ AND: [accessFilter, userFilter] }— an access filter of{ authorId: undefined }reaches the client with the key dropped, i.e. it still matches every row.So for an app on
context.db.Post.findMany()— the API this very file and the generated context's own docblock document —query: ({ session }) => ({ authorId: session?.userId })remains the silent match-everything read for an anonymous caller, while this doc now tells the reader it is refused. Same for theanonymous-access-control.mdhunk.Either scope the wording to the composed read surface, or route the legacy merge through the same total resolution.