Skip to content

Implements the GET /work_packages/:id/wiki_page_links endpoint#22834

Open
mereghost wants to merge 15 commits intodevfrom
impl/work-package-page-links-api
Open

Implements the GET /work_packages/:id/wiki_page_links endpoint#22834
mereghost wants to merge 15 commits intodevfrom
impl/work-package-page-links-api

Conversation

@mereghost
Copy link
Copy Markdown
Contributor

This PR covers work packages: OP#73839, OP#73865 and OP#73866

What?

For the endpoint to be fully introduced a couple of new objects needed to be added: PageLinksQuery, ProviderFilters and a way to enrich the Models with some metadata, represented by the PageLinksMetadataService.

How?

It follows the same patterns we used in storages, having services isolating the API and the same processes to build the data.

What is missing?

  1. An actual query to get the page titles
  2. Some more error handling once this this available
  3. Some assumptions where made, such that the commands would return a Result monad.
  4. Some tests run on all pretend behaviour

@mereghost mereghost self-assigned this Apr 20, 2026
Comment thread COPYRIGHT
Comment thread modules/wikis/spec/services/wikis/page_link_metadata_service_spec.rb Outdated
@mereghost mereghost requested a review from a team April 20, 2026 15:21
@mereghost mereghost force-pushed the impl/work-package-page-links-api branch 3 times, most recently from 664b07e to 431d253 Compare April 24, 2026 12:22
Copy link
Copy Markdown
Contributor

@NobodysNightmare NobodysNightmare left a comment

Choose a reason for hiding this comment

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

This PR looks incomplete. I see (partial) references to a query that doesn't exist.

Maybe it just needs to be committed? Or it was intended for a follow-up PR?

Comment thread modules/wikis/app/models/queries/wikis/page_links/filter/provider_filter.rb Outdated
Comment thread modules/wikis/app/services/wikis/adapters/providers/internal/registry.rb Outdated
Comment thread modules/wikis/app/services/wikis/page_link_metadata_service.rb Outdated
Comment thread modules/wikis/app/services/wikis/page_link_metadata_service.rb Outdated
Comment thread modules/wikis/app/services/wikis/page_link_metadata_service.rb Outdated
@mereghost mereghost force-pushed the impl/work-package-page-links-api branch 2 times, most recently from 671af8f to eddf27d Compare April 28, 2026 15:38
@mereghost mereghost force-pushed the impl/work-package-page-links-api branch from eddf27d to a59c847 Compare May 5, 2026 16:22
@mereghost mereghost requested review from a team and NobodysNightmare May 6, 2026 08:54
@system_arguments.deep_dup
end
def human_name
::Wikis::PageLink.human_attribute_name(name)
Copy link
Copy Markdown
Contributor

@NobodysNightmare NobodysNightmare May 7, 2026

Choose a reason for hiding this comment

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

🔴 🟡 Typo? undefined local variable or method 'name'?

Suggested change
::Wikis::PageLink.human_attribute_name(name)
::Wikis::PageLink.human_attribute_name(:name)

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.

Wait, this does not seem to be an error. There is a method called name defined and PageLink has no attribute called :name.

But then what is this? A page link has

  • linkable
  • provider(_id)
  • author(_id)
  • type
  • identifier

Is this ever going to be something else than :provider? Do we win anything by passing in name as a method here instead of :provider directly?

Suggested change
::Wikis::PageLink.human_attribute_name(name)
::Wikis::PageLink.human_attribute_name(:provider)

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.

Name comes from the filter base class.

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.

Is this ever going to be something else than :provider? Do we win anything by passing in name as a method here instead of :provider directly?

Repeating this question. To me it still seems much clearer to just reference the attribute name that we are going to to translate. Right now it rather looks like a coincidence to me that the name is usable as an attribute name. But it's incredibly hard to track down, because name comes from whatever is passed in as key during create! of the filter.

This means it's theoretically possible to end up with a ProviderFilter that has a name different than :provider. It's probably unlikely etc. But that's why I don't understand why we'd couple these two things that seem unrelated to each other.

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.

Well... at this point I'm not sure what you expect. human_name must tied obligatory to be defined.

Also #name is used by the underlying structure of query resolution to figure out which filter to instantiate and is tied to the class name, besides being a common idiom used on the codebase.

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.

Editorial note: I started this response in a bad mood. Maybe because I felt unheard or at some point would have expected longer answers from your side as well. Not sure. I left my original thoughts in here uneditied under "Jan is mad", but read those with some grains of salt and pepper. (I leave them in because they still explain why I expect something different here).

Everything under "The constructive part" is nicer.

Jan is mad

Well... at this point I'm not sure what you expect.

I wrote that above: ::Wikis::PageLink.human_attribute_name(:provider).

human_name must tied obligatory to be defined.

What?

Also #name is used by the underlying structure of query resolution to figure out which filter to instantiate and is tied to the class name

None of this is visible when looking at ProviderFilter. I even looked pretty far down the call stack and that name will eventually resolve to provider is something that I was able to guess from context (because what else should the human name resolve from?), but not able to see from code.

I am surprised that my wish to understand code from looking at a single file seems to be an alien thing.

besides being a common idiom used on the codebase.

I get the feeling that I will only feel bad feelings when looking at idioms and practices around these filters...

The constructive part

You are right, that you are following mostly a common idiom. Actually a lot of implementations go as far as writing

  def human_name
    model.human_attribute_name(name)
  end

This is interesting, because we have exactly the same code working across a broad range of models. IMO this could be a refactoring that could be done to filters.

Imagine someone would change the base class implementation of human_name from

  def human_name
    raise SubclassResponsibilityError
  end

to

  def human_name
    model.human_attribute_name(name)
  end

This would mean that subclasses don't need to idiomatically implement something that they don't understand (subclasses don't seem to have other uses for name and they don't directly choose it). They only need to overwrite it, if they prefer a different human name, but there would be a sane default.

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.

Bad mood @NobodysNightmare is funny. XD

But yeah, I wasn't able to communicate, because I was in a bad mood for other reasons (IRL) that your complaint would be probably better addressed with a refactor of filters.

No harm done. <3

Comment thread modules/wikis/app/models/queries/wikis/page_links/filter/provider_filter.rb Outdated
Comment thread modules/wikis/app/services/wikis/page_link_metadata_service.rb Outdated
Comment thread modules/wikis/app/services/wikis/page_link_metadata_service.rb Outdated
Comment thread modules/wikis/app/services/wikis/page_link_metadata_service.rb Outdated
Comment thread modules/wikis/lib/api/v3/page_links/page_link_representer.rb Outdated
end
end
unless query.valid?
message = I18n.t("api_v3.errors.missing_or_malformed_parameter", parameter: "filters")
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.

🟡 The ParamsToQueryService deal with different parameters:

  • filters
  • sortBy
  • groupBy

How do we know that an invalid query is caused by filters? Couldn't it also be that a client tried to sort by something and messed the params for that up?

Shouldn't we be able to get from the query what was causing it to be invalid?

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.

Good question, this is the pattern used around the code. =/

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.

O.O

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.

I've looked into this, because this comment still didn't receive any update. Queries do validate a bunch of attributes:

  • :filters
  • :orders
  • :group_by

Looking for usages of the api_v3.errors.missing_or_malformed_parameter translation, I agree to you, that it is only ever called with parameter: "filters". I can't tell you how I feel for all of this API code.

My current suspicion is that filters are the only thing that are changeable from the client side and order and group by come magically from inside OpenProject. While this is not at all visible when interacting with an interface like query.valid? it didn't seem to stop people from either using knowledge of this implementation detail or cargo culting their way around missing knowledge.

I am giving up here, feel free to resolve this discussion if you think that this cargo cult is idiomatic, or add a fix that bases the parameter variable on facts derivable from the validation errors.

Comment thread modules/wikis/lib/api/v3/page_links/work_package_wiki_page_links_api.rb Outdated
@mereghost mereghost force-pushed the impl/work-package-page-links-api branch from 2b90586 to ac872fa Compare May 7, 2026 08:59
Comment thread modules/wikis/lib/api/v3/page_links/work_package_wiki_page_links_api.rb Outdated
mereghost and others added 3 commits May 7, 2026 11:30
Co-authored-by: Jan Sandbrink <j.sandbrink@openproject.com>
@mereghost mereghost force-pushed the impl/work-package-page-links-api branch from 04d42e4 to 836e0bf Compare May 7, 2026 10:02
Comment thread modules/wikis/app/services/wikis/page_link_metadata_service.rb Outdated

# @return [ServiceResult<ActiveRecord::Relation<Wikis::PageLink>]
def call
metadata = relation.group_by(&:provider).flat_map do |provider, page_links|
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.

🟡 I just realized that the group_by(&:provider) is not needed at all since we fetch results one by one anyways.

It would be useful if we were trying to obtain infos in bulk, but given that we are getting them one by one, we could as well get away without the grouping.

Comment thread modules/wikis/app/services/wikis/page_link_metadata_service.rb Outdated
Comment thread modules/wikis/spec/services/wikis/page_link_metadata_service_spec.rb Outdated
…pec.rb

Co-authored-by: Jan Sandbrink <j.sandbrink@openproject.com>
@mereghost mereghost force-pushed the impl/work-package-page-links-api branch from ecae20e to 1f42c3f Compare May 8, 2026 09:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants