fix(plugin-auth): convertWhere() 补齐 not_in / starts_with / ends_with,未识别算子改为响亮拒收 (#5813) - #5844
Merged
Merged
Conversation
…unknown operators (#5813) `convertWhere()` covered eight of better-auth's eleven where operators. The other three — `not_in` / `starts_with` / `ends_with` — fell off the end of its `if / else if` chain: no key was written into `filter`, nothing was logged, and a `where` carrying only such a condition compiled to `{}`. A dropped predicate WIDENS a result set rather than narrowing it, on the identity tables and through a mounted admin route: - `GET /api/v1/auth/admin/list-users?searchOperator=starts_with` answered with every user; `filterOperator`'s enum is the whole vocabulary, so `not_in` excluded nobody. - `update` / `delete` / `consumeOne` / `incrementOne` resolve their target with `findOne(filter)` first, so `{}` picked an arbitrary row and the write landed on the wrong record — measured: deleting "the user whose name starts with zed" removed the table's first row instead. Two halves: 1. The three operators translate to `$nin` / `$startsWith` / `$endsWith`, all members of the spec's `FILTER_OPERATORS`. Case semantics agree on both sides (better-auth's `Where.mode` defaults to `sensitive`; the `$startsWith` family is case-sensitive at the contract layer per #5701 Q2=A), so the direct translation opens no contract seam. 2. The chain tail now THROWS instead of skipping — the #3948 restore-invariant discipline. The message names the operator, the field and the supported set. `never` narrowing in the `default` arm makes a twelfth upstream operator a compile error as well. `Where.mode` is out of scope and deliberately not refused: it is a sibling field of `operator`, not a member of the vocabulary (#5814). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
… with `as any`
`check:query-options-erasure` counts a test-side `find(o, { … } as any)` too
(#4674/#4918), and the ratchet is at its ceiling — the read-back in the
write-path case was on-contract all along, so it is spelled with a real
`QueryAST` rather than raising the count.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
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.
Fixes #5813
前提复核(改之前)
按分诊要求,不按 issue 正文的旧行号,而是对
origin/main@f886f291a(#5710 的 PR #5812 合并之后)重读convertWhere():eq/ne/in/gt/gte/lt/lte/contains八支,contains支已发$contains(fix(plugin-auth): better-auth 的contains下译为$contains,比较值不再当正则求值 (#5710) #5812 的成果,本 PR 原样保留其注释);not_in/starts_with/ends_with零命中,链尾无else兜底 —— 前提成立。活体调用方也实测过,不是转述 issue:装好的
better-auth@1.7.0-rc.2里dist/plugins/admin/routes.mjs:311-322的searchOperator枚举是contains | starts_with | ends_with、filterOperator枚举就是z.enum(whereOperators),而:360/:365把它们直接推进where。算子词表(@better-auth/core/db/adapter)运行时读出来确认是十一个:eq, ne, lt, lte, gt, gte, in, not_in, contains, starts_with, ends_with。改了什么
一、三个算子按表直译。 三个 ObjectQL 算子都在
FILTER_OPERATORS里(五后端都必须求值):not_in$ninstarts_with$startsWithends_with$endsWith二、链尾未识别算子响亮抛错。 错误信息带算子名、字段名、受支持算子清单和落点文件 —— 信息本身就是操作指引。这是 #3948 一族的 restore-invariant:丢谓词不是把结果变窄而是变宽,「悄悄回答另一个问题」比「拒绝回答」更坏。
if / else if链改成switch,顺带拿到第二道锁:default臂里operator收敛为never,所以 better-auth 长出第十二个算子而这里没加case时,pnpm --filter @objectstack/plugin-auth typecheck会先于任何查询报错。运行时那一支拿到的仍是真实算子字符串,进错误信息。关于
operator缺省。Where.operator在 better-auth 侧声明为可选、@default eq,其 factory 的transformWhereClause会在适配器看到之前把默认值物化(CleanedWhere因此是必填形态)。已废弃的裸createObjectQLAdapter收的是手搓 where,可能没走过 factory,所以缺省在这里显式补上 —— 这是生产方自己声明的契约,不是宽容别名;凡是写出来的算子,必须在词表里。大小写语义
starts_with/ends_with两侧同向,直译不开契约缝:better-auth 的Where.mode默认"sensitive",$startsWith/$endsWith按 #5701 Q2=A 在契约层也是大小写敏感。反向验证:方向是普通的「先红后绿」
把三个
case删掉、default改回静默break(即修复前的语义),27 条里红 17 条,方向与预期一致。两条最有说服力的实测输出:第一条是 #5813 最锋利的一半:对四行表执行「删除
name以zed开头的用户」,修复前删掉的是表里的第一行u_abc1。第二条说明谓词被丢时count给出的不是「稍微偏大的数」,而是整张表的行数。测试:三面加一尾
新文件
packages/plugins/plugin-auth/src/auth-where-operator-coverage.test.ts,沿用 #5812 在auth-contains-filter.test.ts建立的两面模式,并补了第三面:$nin/$startsWith/$endsWith,并单独断言not.toEqual({})—— 空 filter 是本缺陷的确切形状;另有count路径与「与邻居条件共存而非互相覆盖」两条。@better-auth/core/db/adapter运行时读出whereOperators,逐个跑过适配器,断言每个都留下谓词;并把源码里的SUPPORTED_WHERE_OPERATORS与之钉成同一集合。这是唯一能看见上游长出新算子的一面 —— 另外两面只测得到有人记得写 case 的算子,而那正是 plugin-auth:convertWhere()对not_in/starts_with/ends_with没有分支,谓词被整条丢弃 —— 认证路径上的过滤放大 #5813 的成因。InMemoryDriver):每个算子一条有排除力的用例(starts_with 'abc'不命中只是包含abc的x_abc;ends_with 'abc'反过来只命中它;not_in真的排除列出的行),外加count与前述写路径。'fuzzy',factory 适配器与裸适配器两条路径都抛错,错误信息含算子名、字段名与受支持清单;并断言读根本没发生(拒收就是不回答,不是回答一个更宽的问题)。行为面的 fake engine 只声明用得到的动词,
delete以assertEngineDeleteDispatch(options)开头(check:engine-double-contract已确认收录为pinned [delete])。关于 #5814 的接缝(分诊指定必答项,报告里同答)
结论:变简单,不变难。 理由见下,并且本 PR 特意留了一条 pin 说明边界。
mode是operator的兄弟字段,不是算子成员,所以链尾拒收读的是operator而不波及mode—— 今天mode: 'insensitive'仍被忽略(那就是 #5814)。有一条测试专门钉住这个边界:带mode: 'insensitive'的查询不抛错、照常译出$startsWith。接缝的形状是这样的:#5814 落地时,每个
case臂是「这个算子在 insensitive 下译成什么」的落点(例如contains+ insensitive →$icontains,#5701 已经把它加进算子词表),而default臂是「这个 mode 在这个算子上无法表示」时该走的拒收位置 —— 同一条接缝,往里一层。改造前它是一串if / else if加一个隐形的静默出口,mode只能靠再加一串平行分支来接;现在它是一个带穷尽性检查的switch,加一维只需要在既有臂里展开,而且漏掉哪一臂会被never和词表面测试一起报出来。唯一需要注意的顺序:#5814 的裁决若引入新的 ObjectQL 算子映射,
SUPPORTED_WHERE_OPERATORS与词表面那条集合相等断言不需要动(mode不进算子表);要动的是各case臂内部。范围与不做的事
packages/plugins/plugin-auth/src/objectql-adapter.ts、plugin-auth 内新增一个测试文件、一个 changeset,没有越界。mode不处理(plugin-auth:convertWhere()整体忽略 better-auth 的Where.mode: 'insensitive'(SCIM 会发它) #5814,决策箱中)。$startsWith/$endsWith编成带i标志的正则,而同包的参考匹配器用String.prototype.startsWith(敏感)。那是 drivers:$regex响亮拒收 +$icontains各后端实现(#4706 裁决 B 案 · 驱动半边) #5702「driver 半边」的预算,不是本单的;所以行为面的 fixture 全部小写、没有任何断言依赖这个分歧怎么收敛。验证
注:plugin-auth 的
tsconfig.json仍排除**/*.test.ts(既有 TEST_DEBT 台账项,本 PR 未新增排除),所以上面的typecheck覆盖的是objectql-adapter.ts。新测试文件另行单独跑过tsc --noEmit确认无类型错误。🤖 Generated with Claude Code
https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
Generated by Claude Code