Skip to content

fix(media): apply the Searcher to every media_libraries query - #1102

Merged
Transmigration-zhou merged 6 commits into
mainfrom
feat/media-searcher-all-queries
Jul 30, 2026
Merged

fix(media): apply the Searcher to every media_libraries query#1102
Transmigration-zhou merged 6 commits into
mainfrom
feat/media-searcher-all-queries

Conversation

@Transmigration-zhou

@Transmigration-zhou Transmigration-zhou commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem

media.Builder.Searcher is the only hook an app has to scope the media library (multi-tenant isolation, per-team libraries, …), but it was applied in exactly one place: mediaLibraryFilter, which backs the listing grid, the chooser dialog, search and pagination.

Every other query in the package addressed rows by id or parent id straight off the request and never consulted the searcher. With a searcher configured for data isolation, a request could still reach rows outside its scope:

Path Effect
chooseFile (filechooser.go) read back any row by id (url, filename, description) and attach it
loadImageCropper / cropImage (cropper.go) load and overwrite any image by id
folderGroupsComponents (media_box.go) Move-to dialog tree lists all folders; expansion counts all children
moveToFolder move target not validated; rows fetched by raw id
parentFolders (filechooser.go) breadcrumbs resolve arbitrary parent ids
folderComponent (filechooser.go) folder tiles count foreign children
wrapFirst → rename / update-description read rows by raw id
doDelete deletes the raw request ids; reparents children of deleted folders unscoped
presets Edit / Update / DoDelete / DetailingDrawer the model's generic CRUD event funcs address rows by primary key through the DataOperator

Change

  • Builder.scopedDB(db, ctx) — a media_libraries query with the searcher applied. Every read/write listed above now goes through it.
  • Builder.folderIsVisible(ctx, id) — validates an upload / new-folder / move target: root is always allowed, any other folder only when the searcher lets the request see it.
  • Builder.recordIsVisible(ctx, id) — backs new WrapFetchFunc / WrapSaveFunc / WrapDeleteFunc guards on the model's editing and detailing builders, and a WrapSearchFunc guard on its listing, so the generic CRUD and listing events cannot address or enumerate out-of-scope rows. Detailing is reached through the new presets.ModelBuilder.GetDetailing() so that installing media does not enable detailing.
  • doDelete now deletes only the ids its scoped read returned, and scopes the child-reparenting UPDATE.

Not covered by a Searcher

MediaBoxSetterFunc persists whatever MediaBox JSON a form submits, so someone who already knows a URL can still attach it to a field without reading the row. Scoping the reads does not change that, and this PR does not attempt to. Both this and app-side queries against media_libraries are called out in media/README.md.

Backward compatibility

With no Searcher configured every guard is inertscopedDB adds no predicate and both *IsVisible helpers return true, so behavior is unchanged. TestNoSearcherKeepsEveryIDReachable pins this (foreign ids, stale folder ids and a move to a foreign folder all still work).

Two deliberate changes apply in that case as well, both of the same class — an event func no longer proceeds with a zero-value MediaLibrary when the record is missing:

  1. wrapFirst gained an ok return. Previously an ErrRecordNotFound left a zero-id object that rename / updateDescription then Saved, inserting a garbage row.
  2. chooseFile and loadImageCropper return early on a missing record instead of rendering a chooser/cropper for a zero-value row.

Tests

New media/scope_test.go (14 tests, gormx test suite like activity): by-id lookups, folder tree, folder child counts, breadcrumbs, wrapFirst, rename, delete (incl. a mixed own+foreign batch and folder-child reparenting), move-to-folder (incl. move-to-root), foreign-parent upload/new-folder rejection, chooseFile, cropImage, and the no-searcher compatibility contract.

go test ./media   # ok, 14 tests

🤖 Generated with Claude Code

The media Builder's Searcher hook only filtered the listing funnel
(mediaLibraryFilter). Every query addressing rows by id or parent id
bypassed it, so an app using the Searcher for data isolation still
leaked and mutated rows outside the configured scope:

- chooseFile attached any media id to a field
- the cropper loaded and overwrote any image by id
- the Move-to dialog folder tree and its expansion listed all folders,
  and the move target was not validated
- breadcrumbs resolved arbitrary parent ids, folder tiles counted
  foreign children
- rename / update-description read rows by id
- doDelete deleted the raw request ids and reparented children of
  deleted folders regardless of scope
- the model's generic presets CRUD event funcs (Edit, Update, DoDelete,
  DetailingDrawer) address rows by primary key through the DataOperator

Route all of them through a new Builder.scopedDB, and validate upload /
new-folder / move targets with Builder.folderIsVisible. Without a
Searcher configured every guard is inert, so existing apps keep their
current behavior; the only deliberate changes there are that event funcs
no longer proceed with a zero-value MediaLibrary when the record is
missing (wrapFirst previously let rename / update-description Save a
zero-id object, inserting a garbage row).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@deepsource-io

deepsource-io Bot commented Jul 29, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in a3d039f...95a4652 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Go Jul 29, 2026 9:53a.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.55172% with 34 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
media/media_box.go 75.43% 6 Missing and 8 partials ⚠️
media/list.go 71.05% 8 Missing and 3 partials ⚠️
media/builder.go 76.92% 3 Missing and 3 partials ⚠️
media/filechooser.go 83.33% 2 Missing and 1 partial ⚠️
Files with missing lines Coverage Δ
media/cropper.go 70.42% <100.00%> (+4.46%) ⬆️
presets/model.go 89.01% <100.00%> (+0.12%) ⬆️
media/filechooser.go 90.62% <83.33%> (+0.14%) ⬆️
media/builder.go 88.77% <76.92%> (+1.27%) ⬆️
media/list.go 80.00% <71.05%> (-12.60%) ⬇️
media/media_box.go 87.72% <75.43%> (+0.67%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…earcher

Both doc comments read as unconditional visibility guarantees. Say what
they actually guarantee: parity with what the searcher gives the listing,
and that a CurrentUserID-only builder keeps its listing-only filter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@zhangshanwen zhangshanwen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

审查了完整 diff,在分支上跑通了 go build ./...go vet ./media/...go test ./media(14 个测试全过),另外写了两个临时探针验证了下面第 2、3 条,均已证实。

总体判断: 问题定位准确,用 scopedDB 把查询集中收口的方向是对的。建议合并前先处理三点 —— 一处是与本次修复无关的公开行为变更(mm.Detailing()),另两处是新代码在 join 型 searcher 下会静默做错事。细节见行内评论。

行内评论覆盖不到的两点补充在这里:

MediaBoxSetterFunc 没有守卫media/media_box.go:100

PR 描述把 chooseFile 列为「attach any media id to a field」,但真正写入字段的是这个 setter,它只把表单提交的 JSON 扫进来,完全不校验 id。chooseFile 的修复仍然有价值(它才是泄漏对方行 URL/文件名/描述的地方),但对已经知道 URL 的人来说,attach 本身依然做得到。可以不改,但建议在描述里说明,避免把保证说得过强。

loadImageCropper 新加的提前 return r, nil 没有测试media/cropper.go:52

TestCropImageScoped 只覆盖了 cropImageloadImageCropper 这条早返回是本 PR 明确列出的行为变更之一,值得补一个用例。

另外想说:已有测试质量不错,TestDoDeleteScopedMixedBatchTestMoveToFolderScoped 里「移回根目录」那个用例,正是最该被钉住的两个点。

Comment thread media/list.go Outdated
return in(obj, id, ctx)
}
})
mm.Detailing().WrapFetchFunc(func(in presets.FetchFunc) presets.FetchFunc {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[建议合并前修复] mm.Detailing() 不是 getter,这里会静默给 media-library 模型打开 detailing。

presets/detailing.go:60 里会设 mb.hasDetailing = true。我用探针验证过:b.GetPresetsModelBuilder().Info().HasDetailing()main 上是 false,在这个分支上变成 true。对所有安装 media 的应用的影响:

  • presets/presets.go:1213 会新挂一条路由 GET {prefix}/media-libraries/{id}(还会多打一行 mounted url 日志)
  • Detailing() 不传参时会把 mb.editing.FieldNames() 复制进 detailing 的字段列表(presets/detailing.go:56-57
  • presets/listing_compo.go:403-409 会把行点击行为从 actions.Edit 切成 detail 链接/抽屉

这些都不是本 PR 想要的效果,属于对外可见的行为/路由变更。建议在 presets 侧加一个不翻转 hasDetailing 的访问器,或者只在应用确实启用了 detailing 时才装这个守卫。

@Transmigration-zhou Transmigration-zhou Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 a9524eb 修复。在 presets 加了 ModelBuilder.GetDetailing(),只返回 detailing builder、不碰 hasDetailing,这里改用它。TestInstallDoesNotEnableDetailing 断言安装后 HasDetailing() 仍为 false。

守卫本身仍然必要:newDetailing 无条件接上 detailing.FetchFunc(dataOperator.Fetch)registerDefaultEventFuncs 也无条件注册 presets_DetailingDrawer,所以不管 detailing 有没有启用,这个 fetch 都可达。

Comment thread media/media_box.go
if len(deleteFolderIDS) > 0 {
if dbErr = tx.
Model(&media_library.MediaLibrary{}).
if dbErr = mb.scopedDB(tx, ctx).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[建议合并前修复] 这个 UPDATE 会静默丢掉基于 Joins 的 searcher。

scopedDB 现在被用来构造 UPDATE,但 GORM 只在 query 回调里消费 Statement.Joins。我用 db.Joins("join owners on owners.user_id = media_libraries.user_id") 这种 searcher 实测,发出的 SQL 是:

UPDATE "media_libraries" SET "parent_id"=0, "updated_at"='...'
WHERE parent_id in (1) AND "media_libraries"."deleted_at" IS NULL

没有 join、没有隔离条件、也不报错 —— 被删目录下属于别的 tenant 的子行照样被 reparent 了。这正是本 PR 要消除的那类漏洞,在修复代码里又出现了一次。

下面几行的 DELETE 已经是安全写法(先用带 scope 的 SELECT 解析出可见 id,再按显式 id 列表删除),这个 UPDATE 建议照同样的模式改:先 scopedDB 查出可见的子行 id,再按 id 列表 Update

TestDoDeleteReparentScoped 之所以能过,只是因为测试用的 searcher 是个纯 Where

@Transmigration-zhou Transmigration-zhou Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 a9524eb 修复。reparent 改成先用 scoped 查询解析出可见的子行 id,再按 id 列表 UPDATE,和下面的 DELETE 同一套路:

var childIDs []uint64
mb.scopedDB(tx, ctx).Where(qualified("parent_id")+" in ?", deleteFolderIDS).Pluck(qualified("id"), &childIDs)
tx.Model(&media_library.MediaLibrary{}).Where(qualified("id")+" in ?", childIDs).Update("parent_id", 0)

TestJoinSearcherScoped 现在用 Joins 型 searcher 跑完整流程,断言 foreign 子行的 parent 不被改动。

Comment thread media/builder.go Outdated
return true
}
var count int64
if err := b.scopedDB(b.db, ctx).Where("id = ? and folder = true", folderID).Count(&count).Error; err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[建议合并前修复] 未加表名限定的 id 会让 join 型 searcher 直接报错,而失败表现是「静默拒绝」。recordIsVisible(第 108 行)同样问题。

实测 SQL:

SELECT count(*) FROM "media_libraries" join owners on ... WHERE (id = 1 and folder = true)
-- ERROR: column reference "id" is ambiguous (SQLSTATE 42702)

两个 helper 都把错误吞掉直接 return false,所以探针里 folderIsVisible(ownFolder) 返回了 false:往自己的目录上传、建子目录、任何移动操作都会被当成「record not found」拒掉;recordIsVisible 则会拒掉所有 presets 的 Edit/Update/Delete/Detail。

注意这个歧义是新引入的:原有的 mediaLibraryFilter 从来没按 id 过滤过,它用的是 folderparent_idselected_typecreated_at,碰撞概率远低于 id

两点建议:

  1. 给列名加限定,写成 "media_libraries"."id""media_libraries"."folder"folderComponentfolderGroupsComponentsdoDeletewrapFirst 里新增的裸 idparent_id 条件同理。
  2. 把 searcher 的错误记日志而不是吞掉 —— 现在一报错就退化成硬拒绝,且没有任何诊断信息。

@Transmigration-zhou Transmigration-zhou Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 a9524eb 修复,写法在 95a4652 调整为内联字面量。

本 PR 引入的每一条裸条件都加上了表名限定 —— 两个可见性 helper,加上 folderComponentfolderGroupsComponentsdoDeletewrapFirst

Where("media_libraries.id not in ? and media_libraries.parent_id = ? and media_libraries.folder = true", idS, record.ID)

一开始是用一个 qualified() helper 拼接的,后来去掉了 —— 直接写成完整的 SQL 片段更好读。用的是不带引号的 media_libraries.id,Postgres 下小写标识符与 "media_libraries"."id" 等价。GORM 从主键生成的条件(First(&m, id))本身已带表名限定,所以没动。

第 2 点建议(记日志)我做成了另一种形式:两个 helper 改为返回 (bool, error),调用方向上传播。media 包里没有 logger,抛 error 同样能暴露诊断信息,也更符合 Go 风格 —— 如果你倾向真的记日志,我可以再改。歧义这个 case 由 TestJoinSearcherScoped 覆盖。

Comment thread media/builder.go
// applied, so by-ID lookups and folder-tree queries see the same subset of rows
// the searcher gives the listing. Without a searcher the query is unscoped,
// preserving the historical behavior — including for a builder that only sets
// CurrentUserID, whose listing-only user_id filter is deliberately not extended

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[值得考虑] 只配了 CurrentUserID 的应用,漏洞原样保留。

media/filechooser.go:553-557 在没有 searcher 时会退回 Where("user_id = ?", mb.currentUserID(ctx)),但 scopedDB 完全忽略 currentUserID。所以只配 CurrentUserID 的应用会得到「列表按用户过滤、按 id 访问全局可达」的组合 —— 正是本 PR 要修的同一类泄漏。

doc comment 里已经说明这是有意为之,作为兼容性取舍站得住。但既然 PR 标题写的是 "every media_libraries query",建议在 media/README.md 里明确写出:CurrentUserID 是列表过滤器,不是隔离边界,要做数据隔离请用 Searcher

@Transmigration-zhou Transmigration-zhou Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 a9524eb 补文档:media/README.md 新增 “Scoping the media library” 一节,明确写出 Searcher 是隔离边界、CurrentUserID 只是列表过滤器(行仍可按 id 访问),并列出 searcher 不覆盖的部分(MediaBoxSetterFunc、应用自己写的查询)。

Comment thread media/list.go
func configList(b *presets.Builder, mb *Builder) {
mm := b.Model(&media_library.MediaLibrary{}).Label("Media Library").MenuIcon("mdi-image")
mb.mb = mm
configScopedCRUD(mb, mm)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[值得考虑] 列表的 SearchFunc 仍未收口。

presets/model.go:143mm.Listing().Searcher 接到了通用的 dataOperator.Search。本 PR 包了 Editing/Detailing 的 fetch/save/delete,但没有包 Listing().WrapSearchFunc

ListingCompopresets/listing_compo.go:32 被全局注册为 stateful actionable 类型,因此即使这里的 listing PageFunc 被自定义覆盖,它的 reload/分页/搜索 action 仍可被派发到这个模型上,从而通过未收口的 operator 枚举全部行。

建议要么给 listing 也加上 scope(用 mb.scopedDB 起手写一个 SearchFunc),要么在注释里明确写清为什么这条路径不可达。

@Transmigration-zhou Transmigration-zhou Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

确认可达,已在 a9524eb 修复。我对运行中的应用 dispatch 了 __dispatch_stateful_action__,payload 是 {"compo_type":"*presets.ListingCompo","injector":"media_libraries","method":"OnReload"},返回结果里包含了全部行的行链接,其中有当前 scope 之外的行。

现在 configScopedCRUD 会包 Listing().WrapSearchFunc,加一条把 id 限制在 scoped 查询里的 SQLCondition,用子查询形式传入,这样 Joins 型 searcher 也能保住:

params.SQLConditions = append(params.SQLConditions, &presets.SQLCondition{
    Query: qualified("id") + " in (?)",
    Args:  []interface{}{mb.scopedDB(mb.db, ctx).Select(qualified("id"))},
})

TestListingCompoReloadScoped 钉住这个行为。

Comment thread media/media_box.go Outdated
return
}
old := wrapFirst(mb, ctx, &r)
old, _ := wrapFirst(mb, ctx, &r)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] 这里(以及 rename 里的第 627 行)只是为了拿变更前的快照,却重复跑了一遍完全相同的查询。

在修改前写 old := obj 就够了 —— 只改 DescriptionFileName 这两个字段,浅拷贝足够 —— 省一次查询,也不用丢弃 ok

@Transmigration-zhou Transmigration-zhou Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 a9524eb 修复 —— 两处都改成在修改前 old := obj。只有 Description / FileName 会变,浅拷贝足够 activity 做 diff,省掉一次查询,也不用再丢弃 ok

Comment thread media/media_box.go Outdated
panic(err)
}
// Delete only the rows the scoped query returned, not the raw request ids.
deleteIDs = deleteIDs[:0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] 复用 deleteIDs[:0] 逻辑是正确的,但这个变量在生命周期内先后代表两种含义(请求里的原始 id → scope 过滤后可见的 id)。单开一个 visibleIDs 会更好读。

@Transmigration-zhou Transmigration-zhou Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 a9524eb 修复 —— 请求里的原始 id 是 requestIDs,scope 过滤后的是 visibleIDs,不再复用同一个变量。

Comment thread media/scope_test.go Outdated
require.Zero(t, reload(t, ownFile.ID).ParentId, "moving to root must keep working")
}

func TestUploadAndCreateFolderRejectForeignParent(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] 测试名叫 TestUploadAndCreateFolderRejectForeignParent,但实际只跑了 createFolderuploadFile 新加的 folderIsVisible 检查没有覆盖(需要构造 multipart body)。

建议要么补上 upload 的用例,要么把名字改成只反映 createFolder

@Transmigration-zhou Transmigration-zhou Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 a9524eb 修复 —— 改名为 TestCreateFolderRejectsForeignParent,并新增 TestUploadFileRejectsForeignParent。不需要构造 multipart:folderIsVisible 的检查在 MustUnmarshalForm 之前,所以空请求就能覆盖这个守卫。

Comment thread media/scope_test.go Outdated
require.True(t, b.folderIsVisible(ctx, foreignFolder.ID))
require.True(t, b.folderIsVisible(ctx, 4242), "a stale folder id stays acceptable")
require.True(t, b.recordIsVisible(ctx, fmt.Sprint(foreignFile.ID)))
require.True(t, b.recordIsVisible(ctx, "4242"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] 整个测试文件里没有端到端覆盖 configScopedCRUD

recordIsVisible 只被直接断言过,而且只在这里「无 searcher」的场景下。但 presets CRUD(Edit / Update / DoDelete / DetailingDrawer)这条路径恰恰是 PR 描述里最先强调的修复点,建议补一个走 mm.Editing().Fetcher/Saver/Deleter 的用例,确认带 searcher 时越权 id 会被拒。

@Transmigration-zhou Transmigration-zhou Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 a9524eb 修复 —— 新增 TestPresetsCRUDEventsScoped,通过 presets.Builder.ServeHTTP 驱动 presets_DoDeletepresets_DetailingDrawer,断言 foreign 行不会被删也不会被读回,而 own 行仍然能正常删除。

- presets: add ModelBuilder.GetDetailing so the media model's detailing
  event funcs can be guarded without calling Detailing(), which sets
  hasDetailing and would mount a detail route and change listing row
  behavior for every app installing media.
- doDelete: resolve the children of deleted folders through the scoped
  query and reparent them by id. GORM only applies a searcher's Joins to
  SELECTs, so scoping the UPDATE directly dropped the condition and
  reparented other scopes' rows.
- Scope the listing's generic search too. ListingCompo actions stay
  dispatchable even though the media library replaces the listing page,
  so its reload/paging/search enumerated every row.
- Table-qualify the raw id / parent_id / folder conditions. A searcher
  adding a Joins clause made them ambiguous, and both visibility helpers
  turned that error into a silent deny, rejecting operations on rows the
  request owns.
- folderIsVisible / recordIsVisible now return the error instead of
  swallowing it.
- updateDescription / rename: snapshot the row in memory rather than
  querying it twice.
- doDelete: name the scoped id set visibleIDs.
- README: document that Searcher is the isolation boundary and
  CurrentUserID is not, and what a searcher does not cover.

Tests: join-based searcher, presets CRUD events end to end, ListingCompo
reload, detailing stays disabled, loadImageCropper early return, upload
into a foreign parent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Transmigration-zhou

Transmigration-zhou commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

已推 a9524eb + 2494e30 + 95a4652,逐条对应:

建议合并前修复

  1. mm.Detailing() 翻转 hasDetailing —— 在 presets 加了 ModelBuilder.GetDetailing(),只返回 detailing builder、不动 flag,media 侧改用它。守卫本身仍然必要:newDetailing 无条件接上 dataOperator.Fetchpresets_DetailingDrawer 也无条件注册,所以不管 detailing 有没有启用,那个 fetch 都可达。TestInstallDoesNotEnableDetailing 钉住安装后 HasDetailing() 仍为 false。

  2. reparent 的 UPDATE 丢掉 Joins searcher —— 改成先用 scoped 查询 Pluck 出可见子行 id、再按 id 列表 UPDATE,和下面的 DELETE 同一套路。TestJoinSearcherScopedJoins 型 searcher 跑完整流程,断言 foreign 子行的 parent 不被改。

  3. 未限定的 id 让 join searcher 报错、且退化成静默拒绝 —— 本 PR 引入的所有裸条件都加了表名限定(两个 helper 加 folderComponentfolderGroupsComponentsdoDeletewrapFirst),写成内联字面量如 Where("media_libraries.id in ?", ids)。GORM 从主键生成的条件(First(&m, id))本身已带表名限定,没动。两个 helper 改为返回 (bool, error),调用方向上传播,searcher 出错会暴露出来而不是变成 “record not found”(这条对应你建议的第 2 点,media 包里没有 logger,抛 error 同样能暴露诊断信息 —— 如果倾向真的记日志我可以再改)。

值得考虑

  1. listing 的 SearchFunc —— 先确认可达:对运行中的应用 dispatch __dispatch_stateful_action__OnReload),返回的行链接包含当前 scope 之外的行。现在 configScopedCRUD 会包 Listing().WrapSearchFunc,加一条把 id 限制在 scoped 子查询里的 SQLCondition,子查询形式让 Joins 型 searcher 也成立。TestListingCompoReloadScoped 钉住。

  2. CurrentUserID —— 写进 media/README.mdSearcher 是隔离边界,CurrentUserID 只是列表过滤器、行仍可按 id 访问,并列出 searcher 不覆盖的部分。

MediaBoxSetterFunc —— 同意,真正写字段的是 setter,它不校验 id,本 PR 没改也不打算改。已在 README 和 PR 描述里明确列为「searcher 不覆盖」,PR 描述里 chooseFile 那条也改成「按 id 读回整行(url/文件名/描述)并 attach」,不再声称能阻止 attach 已知 URL。

nit —— updateDescription/rename 改用 old := obj 省掉重复查询;doDelete 拆成 requestIDs/visibleIDs;测试改名 TestCreateFolderRejectsForeignParent 并新增 TestUploadFileRejectsForeignParentfolderIsVisibleMustUnmarshalForm 之前,空请求就能覆盖,不用构造 multipart);补 TestLoadImageCropperScoped

新增测试:TestJoinSearcherScoped(用 Joins 型 searcher 跑完整流程)、TestPresetsCRUDEventsScoped(通过 presets.Builder.ServeHTTP 驱动通用事件)、TestListingCompoReloadScopedTestInstallDoesNotEnableDetailinggo test ./media 20 个用例全过,./presets/..../pagebuilder/... 无回归。

Transmigration-zhou and others added 2 commits July 29, 2026 18:35
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The qualified() helper obscured what the conditions actually are; the
literals read as plain SQL fragments.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Transmigration-zhou
Transmigration-zhou merged commit eec11ce into main Jul 30, 2026
10 checks passed
@Transmigration-zhou
Transmigration-zhou deleted the feat/media-searcher-all-queries branch July 30, 2026 07:12
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.

3 participants