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

[Federation Feature Proposal]: Pass data to entity resolvers without exposing the data publicly. #365

Open
mcohen75 opened this issue Dec 19, 2019 · 7 comments

Comments

@mcohen75
Copy link
Contributor

At Indeed we've had good success using Apollo Federation across a few teams. We're excited about the benefits here and expect to broaden adoption in the coming months.

As we've on-boarded new teams we've consistently encountered a problem providing the data necessary to federated services via entity loaders. In order to retrieve entities, sometimes data that we would not otherwise expose externally is required. If we consider the set of services that a federated GraphQL API replaces this is not a surprise. An RPC call to a backend service may include data that is not exposed to external API consumers.

The @required directive is sufficient to solve this problem in some situations. However, this data sometimes does not belong on the extended type. In some cases this data is private and should not be exposed externally.

One workaround here is to forgo creating some Federated Services and communicate with backend services directly. The data would then be exposed as a type by one or more Federated Services. This path has the disadvantage that we are less able to create small focused services and instead have more monolithic GraphQL servers.

Another workaround we've employed here is to encode data into the key field of the entity. The Federated Service that owns the extended type creates the encoded key value. The federated service that owns the data and implements the entity loader decodes the key value in order to load entities. The two services agree on the format of the key. Though this workaround gets the job done, it requires coordination across federated services and is error prone. It is also limited in that 1) keys can be large and 2) encoding protected data requires encryption.

An ideal solution to this problem would:

  1. be declarative - Federated services exposing entities for use as extensions should have the ability to describe the inputs required. External manual coordination should not be necessary.
  2. not affect the Graph in a negative way - There should be no artifacts of the integration across schema exposed publicly. For example, it should not be necessary to expose fields or types externally to accomplish a schema extension.
  3. protect private data - It should not be necessary to expose private data to facilitate type extensions.

I'd like to propose a solution that ticks many of these boxes and looks like a reasonable way to address this deficiency.

  1. Introduce a new directive to mark fields and types as private.
    directive @Private on FIELD_DEFINITION | OBJECT

  2. Hide data and schema marked with the @Private directive at the federation server.
    a. Exclude private data from responses.
    b. Exclude private types and fields from introspection query responses.

This would be used as follows:

  1. Add fields and types to the extended schema to expose the data required by the entity loader.
  2. Mark appropriate fields and types with the @private directive.

The following modified Product with shipping estimate example illustrates the concept:

// Product Schema
type Product @key(fields: "upc") {
  upc: String!
  name: String
  weight: Int! @private
  sizeClass: Int! @private
}

// Shipping schema
extend type Product @key(fields: "upc") {
  upc: String! @external
  weight: Int @external
  sizeClass: Int! @external
  shippingEstimate: Int @requires(fields: "weight sizeClass")
}

And the following illustrates the concept with a private type:

// Product Schema
type ProductSpecifications @private {
  weight: Int!
  sizeClass: Int!
}

type Product @key(fields: "upc") {
  upc: String!
  name: String
  productSpecifications: ProductSpecifications!
}

// Inventory / Shipping Schema
extend type Product @key(fields: "upc") {
  upc: String! @external
  weight: Int @external
  price: Int @external
  inStock: Boolean
  shippingEstimate: Int @requires(fields: "productSpecifications")
}

This solution does not define the inputs required by the entity loader in a declarative way. Although it would be very useful to do so, this solution addresses the most important parts of the problem I've expressed here.

@mcohen75 mcohen75 changed the title [Federation Feature request]: Pass data to entity resolvers without exposing the data publicly. [Federation Feature Request]: Pass data to entity resolvers without exposing the data publicly. Dec 19, 2019
@mcohen75
Copy link
Contributor Author

@trevor-scheer @abernix I'm interested in hearing your thoughts about something like this. I'm willing to help implement this if it's a direction that you think makes sense.

@mcohen75 mcohen75 changed the title [Federation Feature Request]: Pass data to entity resolvers without exposing the data publicly. [Federation Feature Proposal]: Pass data to entity resolvers without exposing the data publicly. Dec 26, 2019
@Superd22
Copy link

Just fyi, I was under the impression that this was on the roadmap in the form of an Internal directive. but cannot find any mention of it besides that comment

@mcohen75
Copy link
Contributor Author

mcohen75 commented Jan 6, 2020

Thanks @Superd22 for referencing that comment! I haven't been following the federation-demo project so was not aware of these plans. Hopefully this issue will serve as a useful place to discuss this feature.

@abernix
Copy link
Member

abernix commented Mar 10, 2020

@mcohen75 Does https://github.com/apollographql/apollo-server/issues/2812 sound like it may parallel the ideas you're suggesting here?

@mcohen75
Copy link
Contributor Author

@mcohen75 Does apollographql/apollo-server#2812 sound like it may parallel the ideas you're suggesting here?

Yes, it does. One key difference here is that I've proposed that the same directive should apply to types as well. This will enable nesting to avoid muddling a type with parameters that are out of place.

@benkeil
Copy link

benkeil commented Mar 25, 2020

You could solve this with a workaround. Given the channel API returns an accountRef field you can do:

Schema:

type Account {
    id: String
}
type Channel {
    account: Account
}

The account Field resolver in the channel resolver:

export const account = (parent: Channel & { accountRef: string }, _: any, context: Context): Account => (
  {
    id: parent.accountRef,
  }
);

@paulpdaniels
Copy link

Introduce a new directive to mark fields and types as private.
directive @Private on FIELD_DEFINITION | OBJECT

Was going to open an issue, glad I searched first. I just ran into this issue as well and would like the ability to selectively declare certain fields as private (or scoped) such that we can use them when talking across service boundaries but remain hidden from introspection and external use.

@abernix abernix transferred this issue from apollographql/apollo-server Jan 15, 2021
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

No branches or pull requests

5 participants