apollographql / apollo-kotlin

:rocket:  A strongly-typed, caching GraphQL client for the JVM, Android, and Kotlin multiplatform.
https://www.apollographql.com/docs/kotlin
MIT License
3.77k stars 656 forks source link

Support the notion of partially sharing types between 2 schemas #5951

Open ychescale9 opened 5 months ago

ychescale9 commented 5 months ago

Use case

Imagine a monorepo with 2 apps, where each app has its own endpoint and graphql schema.

:schema-1 // graphql schema for app 1
:schema-2 // graphql schema for app 2 
:feature-1-a // app 1's feature module
:feature-1-b // app 1's feature module
:feature-2-a // app 2's feature module
:feature-2-b // app 2's feature module
:app-1
:app-2

We want to build an app-agnostic server-driven UI framework :sdui where we have Composables that depend on some common types in both the 2 schemas:

// common part of the schema for sdui
type Button {
  style: ButtonStyle!
  label: String!
}
enum ButtonStyle {
  PRIMARY
  SECONDARY
  TERTIARY
  DESTRUCTIVE
}

The :sdui module would include graphql fragments:

fragment SduiButtonData on Button {
  style
  label
}

and Composable functions that render the generated fragment:

@Composable
public fun SduiButton(
    buttonData: SduiButtonData,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
   ...
}

The problem is since each schema needs to have its own service, a downstream module (:sdui) would have to choose to use one of the 2 schemas (and service) in order to get the metadata to for generating the code for the fragment, in order to avoid duplicated types from the 2 schemas. This also has a cascade effect where the consumers of :sdui also needs to add the same service / schema as the ones used in :sdui, so if a feature module uses a different schema / service than sdui it would have to depend on both schemas and declare both services, which expose a lot of types and queries that aren't relevant to the feature module and its app.

Describe the solution you'd like

I'm not sure exactly what solution I'd like TBH. Perhaps a mechanism to merge a "synthetic" schema from a 3rd module into both schema modules' services, and also allow a library to depend on the "synthetic" module for declaring fragment and generating code that can be consumed by downstream modules that depend on either schema / service?

I've played with a couple of ideas but didn't get far:

  1. write compiler plugin to add common interface for the SDUI related types, for both schema, so the SDUI library can depend on the interface types, while the downstream feature modules can cast the concrete type to the interface type when using the SDUI components
  2. parse the schema document after downloading either schema (using the tooling artifacts?) and extract the parts relevant to SDUI into a synthetic / stub schema that both schema modules include
BoD commented 5 months ago

Thanks for reporting this. I don't have an immediate idea of what's the best course of action for you (I think maybe the question can be generalized to "how to share code from 2 separate schemas with common types"), but SDUI in general is definitely something we want to keep under our radar.

martinbonnin commented 5 months ago

Quick note: I think my favorite solution there would be a single schema. That would have 2 issues:

  1. you can use “schema-1” types from module “feature-2"
  2. there is some coupling between "feature-1" et "feature-2" (when you reference a new type from "feature-2" , you might end up recompiling "feature-1")

For 1. I wonder if we could use some tooling to restrict types for consumers, potentially based on @requiresOptIn.

For 2., I'm not sure unless we introduce the possibility to generate types in "intermediate" modules compared to today generating all schema types in the "schema" module.