Skip to content

feat: align saved filter API shape, add saved_filter_id support and dynamic label mapping#6702

Open
notsafeforgit wants to merge 2 commits intostashapp:developfrom
notsafeforgit:fix/saved-filter-api-shape
Open

feat: align saved filter API shape, add saved_filter_id support and dynamic label mapping#6702
notsafeforgit wants to merge 2 commits intostashapp:developfrom
notsafeforgit:fix/saved-filter-api-shape

Conversation

@notsafeforgit
Copy link
Copy Markdown
Contributor

This PR refactors how saved filters are stored and handled in the API to align them with standard input shapes. It also introduces the ability to query results directly using a saved_filter_id, which simplifies UI state management and improves performance.

Importantly, this change migrates labels from static storage within the filter JSON to a dynamic resolver, ensuring that labels (e.g., Tag names, Performer names) are always up-to-date even if the underlying objects are renamed.

Key Changes

1. API Shape Alignment & Migration

  • Serialized Shape: The object_filter in the saved_filters table has been refactored to match the GraphQL input types (e.g., SceneFilterType). Previously, it stored a more complex structure that included hardcoded labels, which led to stale data.
  • Migration (v86): Added a migration and a Go post-migration script to automatically convert existing saved filters. This process strips the static labels and converts the criteria into the strictly-typed format used by the API.

2. saved_filter_id Query Parameter

  • Added a saved_filter_id parameter to all find* queries (Scenes, Images, Performers, Studios, etc.).
  • Functional Logic: When provided, the API resolves the saved filter by ID, enforces that the mode matches the query type, and merges it with any provided filter (FindFilter) overrides like search string, paging, or sorting.

3. Dynamic Label Mapping Support

  • Live Resolvers: Added a label_mapping field to the SavedFilter type that is resolved dynamically at request time.
  • Data Consistency: Because labels are no longer stored in the database's JSON blob, the UI will always display the current name/title of a referenced object. If a Tag or Performer is renamed, all saved filters referencing that object will reflect the change immediately.
  • Reduced Redundancy: The database now only stores the IDs of filtered objects, reducing the size of the object_filter blob and following better data normalization practices.

4. UI Integration

  • Updated UI GraphQL fragments and models to support the new label_mapping and the simplified object_filter structure.
  • Fixed TypeScript type mismatches resulting from the schema changes.

Testing

  • Migration Testing: Added pkg/sqlite/migrations/86_postmigrate_test.go to verify the conversion of various saved filter configurations.
  • Resolver Testing: Added internal/api/resolver_saved_filter_helper_test.go to verify:
    • Successful resolution and mode-enforcement of saved filters.
    • Correct merging of FindFilter overrides.
    • Successful extraction and resolution of label_mapping for tags, performers, and other entities.
  • Verification: Ran make validate (UI lint/check and backend lint/tests) and make it (integration tests).

@notsafeforgit notsafeforgit changed the title fix: align saved filter serialized shape with API standards feat: align saved filter API shape, add saved_filter_id support and dynamic label mapping Mar 18, 2026
@Gykes
Copy link
Copy Markdown
Collaborator

Gykes commented Mar 18, 2026

This is your 3rd PR. Lets go ahead and limit the amount that we have open till 1-2. Also, for larger refactors like this please reach out to someone on the dev team before blindly PRing. We like to take things slower around here and stacking all of these on top of each other makes our life harder.

Please reach out on GH, Discord, or Discourse. You can ping me whenever, i'm around

c.value.length > 0 &&
typeof c.value[0] === "string"
) {
this.value = (c.value as unknown as string[]).map((id: string) => ({
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.

You'll notice some "ugly" double-casting. This was a necessary trade-off to support the new normalized ID-only storage format while maintaining the UI's rich model.

The ModifierCriterion<V> class is a generic base used by almost every filter in the UI. For many filters, V is a complex object (like ILabeledId[]), but the raw data coming from the saved filter's object_filter is now just a simple string[] (IDs only).

Because V is a generic type parameter, TypeScript won't allow a direct cast to string[]. We have to cast through unknown to "reset" the type checker so we can map the IDs back into the rich objects ({id, label}) the UI expects. The alternative would be a massive refactor of the entire filter type hierarchy to support union types (V | string[]) at every level. This would have touched dozens of files and significantly increased the risk of regressions.

The current implementation effectively "hydrates" the ID-only storage format back into the rich UI model using the label_mapping provided by the API, keeping the complexity isolated to this single base class.

@notsafeforgit
Copy link
Copy Markdown
Contributor Author

@Gykes sorry about the 3 rapid fire PRs, this is all I have for now. I have just been bothered by these issues in stash for years now and finally got the time to really go heads down for the past week or so and iterate. Despite opening the PRs in quick succession, I spent a lot of time thinking about how to actually go about implementing these changes in a clean way, so they should hopefully not be a nightmare to review.

@notsafeforgit
Copy link
Copy Markdown
Contributor Author

Projects like https://github.com/1letzgo/stashy and https://github.com/secondfolder/stash-tv have long had to hack around the oddity of saved filters returning a different shape to clients vs what the find* apis actually accept, and most apps that use stash's api like these just have broken filters all the time. Hopefully this PR will make clients like them effortless to maintain when it comes to fetching the objects they want out of stash!

@Gykes
Copy link
Copy Markdown
Collaborator

Gykes commented Mar 18, 2026

No problem, I don't want to make it seem like we don't want you making fixes and features, as we appreciate it. Currently we are getting near the end of the release and neither myself or the main dev has put any thought into how to handle this situation. Having a 30 minute convo with the admin team can go a long way of saving both of our times if we prefer a different implementation or just want to defer it completly.

@Gykes Gykes added the deferred Good feature that can be looked at for a later release. label Mar 18, 2026
- Added comprehensive tests for LabelMapping deduplication and full property coverage in resolver_model_saved_filter_test.go
- Removed redundant hardcoded switch [type]Filter statements across 10 query resolvers in favor of direct models.FilterMode assignment
- Cleaned up unused fmt imports generated by the refactor
@notsafeforgit notsafeforgit force-pushed the fix/saved-filter-api-shape branch from 0c39702 to 45ae1c2 Compare March 27, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deferred Good feature that can be looked at for a later release.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants