Skip to content
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

[Request] Ability to enumerate objects by type from store for missing field handlers #4912

Open
alex-statsig opened this issue Feb 14, 2025 · 2 comments

Comments

@alex-statsig
Copy link
Contributor

Missing field handlers provide a great way to normalize data fetched through different field paths.

One such use case supported today is a dropdown/table to select from a list of items (e.g. viewer.tasks { id name}), followed by a unit (or second page) which shows some more information about the selected item (e.g. viewer.task(id) { id name ...TaskDescription }). This can be supported by a handler which returns argValues.id, allowing the second unit/page to partially render instantly

However, a similar use-case not supported could be a unit/page which fetches the task by another property, such as viewer.task(name) { id name ... }. While it would be possible to enumerate store.getRoot().getLinkedRecord('viewer').getLinkedRecords('tasks'), this only works when the initial dropdown fetched exactly viewer.tasks. In practice, it may have fetched viewer.tasks(first: 15), or viewer.tasks(completed: false) (or viewer.completed_tasks). Regardless of the fields involved, the relay store will contain a list of Task objects keyed by their IDs. If there was a function store.getAllByType('Task'), we could enumerate them (albeit somewhat inefficiently, but for a small number of tasks this is fine; perhaps a more robust solution would allow a listener for any Task being updated to maintain this lookup map?) and find the task with the matching name.

I think it's fairly common that the id is not the only method to identify/lookup items (ex. the URL may often contain a friendlier name), so it'd be valuable to lookup data from the store in other ways for missing field handlers. From what I can tell, there is no function exposed to do this today.

@captbaritone
Copy link
Contributor

I don't think there's any way for Relay to support this without either maintaining an internal index of cache records by id, or doing a full scan of all records checking their typename.

I don't think it makes sense for Relay to take on the runtime/memory overhead of maintaining such an index for a smaller use case like this.

I suspect our best bet here is to enable you to implement the full table scan yourself by providing access to the store. I don't think we pass the missing field handler access to the store today. Would it be possible for you to construct your missing field handler such that it closes over your environment/store such that you could manually traverse over all records in this case?

Aside: One avenue that might be worth exploring would be finding a way for Relay to explicitly support some of the directives defined in the Composite Schema spec. For example @lookup. Maybe we could explore something where Relay compiler could insert hints to Relay that for @lookup fields it could/should maintain an additional index? https://graphql.github.io/composite-schemas-spec/draft/#sec--lookup

But that's a larger project.

@alex-statsig
Copy link
Contributor Author

Thanks a ton for the context here, I think I'm able to get a proof of concept working based on this. In particular, defining the field handler in a closure with the store helps, and from there I realized I could actually subclass the RecordSource to maintain the mapping by typename (traversing the entire store could still lead to too much over-scanning). I could probably go further to actually index by my "name" field, but even enumerating by typename should be fine for my use-case.

Here's a quick little example record source which seems to provide the tracking fairly cheaply:

class RecordSourceWithTypeIndexing extends RecordSource {
  _typeNameToDataIDs: Record<string, Set<DataID>> = {};

  handleTypeNameIndexing(dataID: DataID, type: 'add' | 'remove'): void {
    const record = this.get(dataID);
    if (
      record == null ||
      !('__typename' in record) ||
      typeof record.__typename !== 'string'
    ) {
      return;
    }
    const typename = record.__typename;
    if (type === 'add') {
      this._typeNameToDataIDs[typename] ??= new Set();
      this._typeNameToDataIDs[typename].add(dataID);
    } else {
      this._typeNameToDataIDs[typename]?.delete(dataID);
      if (this._typeNameToDataIDs[typename]?.size === 0) {
        delete this._typeNameToDataIDs[typename];
      }
    }
  }

  override set(dataID: DataID, record: Record<string, object>): void {
    super.set(dataID, record);
    this.handleTypeNameIndexing(dataID, 'add');
  }
  override clear(): void {
    super.clear();
    this._typeNameToDataIDs = {};
  }
  override delete(dataID: DataID): void {
    this.handleTypeNameIndexing(dataID, 'remove');
    super.delete(dataID);
  }
  override remove(dataID: DataID): void {
    this.handleTypeNameIndexing(dataID, 'remove');
    super.remove(dataID);
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants