apollographql / federation

🌐  Build and scale a single data graph across multiple services with Apollo's federation gateway.
https://apollographql.com/docs/federation/
Other
661 stars 248 forks source link

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

Open mcohen75 opened 4 years ago

mcohen75 commented 4 years ago

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 commented 4 years ago

@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.

Superd22 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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

mcohen75 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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.