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

[Federated] Services namespaces #350

Open
terion-name opened this issue Aug 22, 2019 · 19 comments
Open

[Federated] Services namespaces #350

terion-name opened this issue Aug 22, 2019 · 19 comments

Comments

@terion-name
Copy link

Currently I have a system built with microservices and with a self-written gateway that uses schema-stitching and a lot of magic.
This system was written several years ago and now needs a refresh and Apollo Federation has most of features that I've implemented by my own but better designed. Now I want to migrate to Federation but it seems to a problem.

My usecase maybe specific but I think it is not uniquely rare.

In short: services should have namespaces.

Namespaces are needed to prevent type collisions. The idea behind is that services are autonomous, are written by numerious developers, including external ones. And services can (and do) have alike types (e.g. News has Article and Blog has Article).

Controlling types uniqueness "administratively" is not an option due to big amount of services and people involved. Also considering auto-generated CRUD types (e.g. from Prisma) this is becoming even more of a problem. To 100% eliminate type collisions and bring a unified convention to end-users gateway should namespace services.

In practice it looks like this:

Services:

# news service
Query {
   articles: [Article!]!
}
Article {
  id
  body
}
# blog service
Query {
   articles: [Article!]!
}
Article {
  id
  content
}

At the gateway schemas are transforming before stitiching and result is like this:

Query {
   News {
      articles: [NewsArticle!]!
   }
   Blog {
     articles: [BlogArticle!]!
   }
}
NewsArticle {
  id
  body
}
BlogArticle {
  id
  content
}

This approach proven to be convenient with tens or hundreds of services with hundreds or thousands of types and with autogeneration on services level.

Looking at Apollo Federation I don't see a possibility to do this. So the questions:

  1. Maybe there is a possibility to transform service schemas that I've overlooked?
  2. Maybe such a feature should be considered like an option? Something like:
new ApolloGateway({
  serviceList: [
    { name: 'news', url: 'http://localhost:4001', namespace: 'News' },
    { name: 'blog', url: 'http://localhost:4001', namespace: 'Blog' }
  ],
});
@terion-name
Copy link
Author

anyone?

@tlgimenes
Copy link

I'm currently having the same problem in my gateway. Managing type conflicts administratively is not an option either. Do you guys have any idea / roadmap /design doc to support this kind of feature or I should try to implement myself ? I the answer is the later, I think I could try implementing and open sourcing the final code

@terion-name
Copy link
Author

@tlgimenes I was thinking abut this a lot and yet didn't figure out how to make this possible.

The simpliest as far as I think now way is to transform (rename types and nest root types) schemas at service level but again - if you don't control it this is not an option.

Make a transforming proxy inbetween gateway and service? Overhead and should break relation resolution (but maybe there is a way to proxy this also)

Fork gateway and apply transforms on schema construction level? Again, relation resolution will fail.

Also there is a place where this can be done (best option maybe) — RemoteGraphQLDataSource. Maybe it is possible to make a datasource that will transform everything, but again - relation resolution and other federation features...

I'll try to experiment with this but at the moment I have no certain understanding of how it is possible

@terion-name
Copy link
Author

My current attempt is to transform schema at loadServicesFromRemoteEndpoint level.

diff --git a/packages/apollo-federation/src/composition/types.ts b/packages/apollo-federation/src/composition/types.ts
index 62538a2c..aa52d64c 100644
--- a/packages/apollo-federation/src/composition/types.ts
+++ b/packages/apollo-federation/src/composition/types.ts
@@ -37,6 +37,7 @@ export interface ServiceDefinition {
   typeDefs: DocumentNode;
   name: string;
   url?: string;
+  namespace?: string;
 }
 
 declare module 'graphql/type/definition' {
diff --git a/packages/apollo-gateway/package.json b/packages/apollo-gateway/package.json
index 2e51962c..fffc9d1b 100644
--- a/packages/apollo-gateway/package.json
+++ b/packages/apollo-gateway/package.json
@@ -28,6 +28,7 @@
     "apollo-server-env": "file:../apollo-server-env",
     "apollo-server-types": "file:../apollo-server-types",
     "graphql-extensions": "file:../graphql-extensions",
+    "graphql-tools": "^4.0.5",
     "loglevel": "^1.6.1",
     "loglevel-debug": "^0.0.1",
     "pretty-format": "^24.7.0"
diff --git a/packages/apollo-gateway/src/loadServicesFromRemoteEndpoint.ts b/packages/apollo-gateway/src/loadServicesFromRemoteEndpoint.ts
index 06a2a0b3..552e0d1b 100644
--- a/packages/apollo-gateway/src/loadServicesFromRemoteEndpoint.ts
+++ b/packages/apollo-gateway/src/loadServicesFromRemoteEndpoint.ts
@@ -1,9 +1,10 @@
 import { GraphQLRequest } from 'apollo-server-types';
-import { parse } from 'graphql';
+import {parse, printSchema} from 'graphql';
 import { Headers, HeadersInit } from 'node-fetch';
 import { GraphQLDataSource } from './datasources/types';
 import { UpdateServiceDefinitions } from './';
 import { ServiceDefinition } from '@apollo/federation';
+import {makeExecutableSchema, RenameTypes, transformSchema} from "graphql-tools";
 
 export async function getServiceDefinitionsFromRemoteEndpoint({
   serviceList,
@@ -13,6 +14,7 @@ export async function getServiceDefinitionsFromRemoteEndpoint({
   serviceList: {
     name: string;
     url?: string;
+    namespace?: string;
     dataSource: GraphQLDataSource;
   }[];
   headers?: HeadersInit;
@@ -27,7 +29,7 @@ export async function getServiceDefinitionsFromRemoteEndpoint({
   let isNewSchema = false;
   // for each service, fetch its introspection schema
   const serviceDefinitions: ServiceDefinition[] = (await Promise.all(
-    serviceList.map(({ name, url, dataSource }) => {
+    serviceList.map(({ name, url, namespace, dataSource }) => {
       if (!url) {
         throw new Error(`Tried to load schema from ${name} but no url found`);
       }
@@ -53,10 +55,25 @@ export async function getServiceDefinitionsFromRemoteEndpoint({
               isNewSchema = true;
             }
             serviceSdlCache.set(name, typeDefs);
+
+            let serviceTypeDefs;
+            if (namespace) {
+              const schema = transformSchema(makeExecutableSchema({typeDefs}), [
+                new RenameTypes(
+                    (typename: string) => `${namespace}${typename}`,
+                    {renameBuiltins: false, renameScalars: true}
+                )
+              ]);
+              serviceTypeDefs = parse(printSchema(schema));
+            } else {
+              serviceTypeDefs = parse(typeDefs);
+            }
+
+
             return {
               name,
               url,
-              typeDefs: parse(typeDefs),
+              typeDefs: serviceTypeDefs,
             };
           }

And it works fine until you try to make relations between services. Then everything fails (based on https://github.com/apollographql/federation-demo):

[nodemon] starting `node gateway.js`
Encountered error when loading inventory at http://localhost:4004/graphql: Cannot extend type "Product" because it is not defined.
Unknown directive "key".
Unknown directive "external".
Unknown directive "external".
Unknown directive "external".
Unknown directive "requires".
Encountered error when loading accounts at http://localhost:4001/graphql: Cannot extend type "Query" because it is not defined.
Unknown directive "key".
Encountered error when loading reviews at http://localhost:4002/graphql: Cannot extend type "Product" because it is not defined.
Unknown directive "key".
Unknown directive "external".
Unknown directive "key".
Unknown type "User".
Unknown directive "provides".
Unknown type "Product".
Cannot extend type "User" because it is not defined.
Unknown directive "key".
Unknown directive "external".
Unknown directive "external".
Encountered error when loading products at http://localhost:4003/graphql: Unknown directive "key".
Cannot extend type "Query" because it is not defined.
(node:28232) UnhandledPromiseRejectionWarning: GraphQLSchemaValidationError: Type Query must define one or more fields.

@terion-name
Copy link
Author

As far as I think now the biggest problem is that service extending types of other services and they could not be correctly namespaced because you don't know from what service is it

@terion-name
Copy link
Author

terion-name commented Oct 8, 2019

Ok, this version namespaces the types and build a proper schema (without nesting but better then nothing)
https://github.com/terion-name/apollo-server/blob/c6f517007ba91be2257087ad8ff89599825242df/packages/apollo-gateway/src/loadServicesFromRemoteEndpoint.ts

Direct queries in services work, relations — don't (return null)

Based on federation-demo (terion-name/federation-demo@51dac69)
Скриншот 2019-10-08 18 01 38

@tlgimenes @abernix a look and/or some suggestions for next moves?

@tlgimenes
Copy link

Can't we use the name of the namespace in the relation directives to make it work ? I still didn't have the time to look at it but I think if we use a parameter of the namespace in the relation directives we could make relations work by correctly transforming the types. And I think that using the namespace in the directive is more than fair, don't you think ?

@terion-name
Copy link
Author

@tlgimenes yeah, in this case directive should use namespaced type.

btw I've didn't manage to make apollo federation to work properly with namespaces (schema ok, but relation requests — not) so I'm building own gateway on top of federation protocol

@elyfialkoff
Copy link

I know this is sort of a side note (but can't figure out where to post). How were you @terion-name able to get the resolvers from the auto-generated CRUD from Prisma.

@queerviolet queerviolet self-assigned this Jan 21, 2020
@queerviolet
Copy link
Contributor

Currently, yes, the recommended strategy is to alter the downstream schema which, yes, is not the best answer if you don't control it. We're looking at various ways to support namespaces rn

@terion-name
Copy link
Author

@queerviolet I've fully rewritten gateway (in fact wrote it from ground up) to get namespaces and other features like advanced customisable relations, multi tenancy etc. This can be done, but I don't think thatt current apollo server gateway design can be altered without massive rewrite

@mikelyonsridgeline
Copy link

this would definitely be a nice to have for on-boarding to federation. i'm currently in the in-between state of can and can't administratively manage the schema.

@joeyaurel
Copy link

Any updates on this?

@SVilgelm
Copy link

@queerviolet Is there any updates on this?

@MayurAccion
Copy link

Any updates on this?

@abernix abernix transferred this issue from apollographql/apollo-server Jan 15, 2021
@kevin-lindsay-1
Copy link

kevin-lindsay-1 commented Aug 6, 2021

Doesn't extend type <Name> @key give you enough information to detect that a particular type exists outside of the schema's namespace?

For example:

# "Loan" service

# The subject of a loan (typically an individual or a business)
type Subject implements Node {
  id: ID!
  ...
}

# The "global" variant of subject, the Auth version of a subject
extend type Subject implements Node @key(fields: "id") {
  id: ID!
 ... @external
}

When running buildFederatedSchema couldn't you detect these via the fact that the type has extend and @key on it, and prep the typedefs for dynamic namespacing via codegen in buildFederatedSchema?

As an example, you might see the type names get converted to something machine-readable like __INTERNAL__Subject and __EXTERNAL__Subject, respectively.

Then, in the gateway, you add the service, with an option for the namespace: "loan" and those type definitions can then be renamed to something more appropriate, such as LoanSubject and Subject, respectively via a rule akin to "Change __INTERNAL__ to <namespace> and remove __EXTERNAL__ from definitions."


While this idea might work, it might not necessarily be the easiest to read, and it might be difficult to understand at a glance from which service this type originates.

Maybe it would be useful for the sake of preventing global namespace collision and readability to specify something like:

# similar to declaring external fields, you can declare the type as external and
# the name of the service it will use in federation
[extend] type Subject implements Node @external(service: "auth") @key(fields: "id") {
  id: ID!
  ... @external
}

Note: extend is in brackets because it might not be necessary, as buildFederatedSchema could theoretically place extend for you at runtime, and code generation/linters would probably need to have federation.enabled: true in order to ignore what would normally be an error in graphql (which is most likely already being done by many users of grahpql-codegen).

This might make manually declaring a service's "namespace" irrelevant, as the gateway should theoretically have enough information to detect and resolve namespace collision this way, with the caveat of it theoretically being necessary to declare an "authoritative" type if a collision occurs.

In other words, by default, definitions are globally scoped and not namespaced (unless maybe you state you want all namespaces to be added 100% of the time when constructing the runtime schema in the gateway, for example serviceList:[{name: "loan", namespace: true}]), and if there's a namespace collision it informs you the action to take, a possible solution described below.

For example, let's say Subject (auth) exists, and then Subject (loan) comes along. It would probably be useful in that instance to be able to state that the auth version was part of the original API, so as to not introducing a breaking change. A directive like "@primary" might be useful in that instance, which would tell the gateway that said type definition is the one that should be able to live in the non-namespaced scope.

Whether or not the @primary type should also create a namespaced variant is a different question, as I imagine some users wouldn't tag any type definitions as @primary until a global namespace conflict arises, at which point you can resolve the conflict by declaring one as Subject @primary(deprecation: Date), and then Subject would appear in the federated schema as type Subject @deprecated("Transition to <Namespace>Subject by <Date>").


As a concrete example:

# Loan Service
type Subject implements Node {
  id: ID!
  ...
}

type Subject implements Node @external(service: "Auth") @key(fields: "id") {
  id: ID!
  ... @external
}

Which would get converted by buildFederatedSchema to something along the lines of:

# Loan Service
type __INTERNAL__Subject implements Node { # or maybe use an under-the-hood directive, like @apolloFederationInternal, instead of changing the name
  id: ID!
  ...
}

type __EXTERNAL__Subject implements Node @external(service: "Loan") {
  id: ID!
  ... @external
}

Which when added to a federated schema:

const serviceList = [{ name: 'Auth', url: '...' }, { name: 'Loan', url: '...' }]

Would produce an error:

Error: namespace collision on "Subject" (Auth) and "Subject" (Loan). Please declare one as @primary and optionally specify a deprecation date.

Because you need to declare one as @primary:

# Auth Service
type Subject implements Node @primary(deprecation: "20XX-01-01") {
  ...
}

At which point the federated schema would look like:

# Auth variant
type Subject implements Node @deprecated(reason: "Transition to AuthSubject by 20XX-01-01") {
  ...
}

# Automatically namespaced Auth variant, only appears if deprecation is enabled on the above variant
type AuthSubject implements Node {
  ...
}

# Automatically namespaced Loan variant
type LoanSubject implements Node {
  ...
}

@hsblhsn
Copy link

hsblhsn commented Oct 23, 2021

Hi, there's a long time no update on this issue. Is there any thoughts or update about it yet?

@richardscarrott
Copy link

richardscarrott commented Feb 8, 2022

Yeah, it would be interesting to hear more about how people are solving this. We built our API gateway using schema stitching ~3 years ago and just blanket namespaced all types + root fields, e.g.

    // api-gateway service
    const rawSchema = await getRemoteSchema(usersLink);
    return transformSchema(rawSchema, [
      new RenameTypes(name => `Users${ucFirst(name)}`),
      new RenameRootFields((operation, name) => `users${ucFirst(name)}`),
    ]);

It's worked fine, but makes our API a bit grim to read, e.g.

query {
   usersUser(id: "usr_123") {
      __typename # UsersUser
      id
      locale {
         __typename # UsersLocale
         region
         language
         currency
      }
   }
   ordersOrder(id: "odr_123") {
       __typename # OrdersOrder
       id
       locale {
          __typename # OrdersLocale
          region
          language
          currency
       }
       user {
          __typename # UsersUser (delegates to the users service)
          id
       }
   }
}

Is this a recommended approach and if so, is it easily done with Apollo Federation? I guess we could just perform the transformation in the subgraph so the gateway doesn't need to know about it?

@korinne
Copy link
Contributor

korinne commented Jan 3, 2023

Hi all, I'm the Product Manager for the Apollo Federation Core team 👋 We're currently researching this request, and we'd love to hear any more feedback/requirements for namespaces in Federation.

There's a lot of great detail in this thread, but we wanted to open the door for more candid feedback -- where we can dive into your specific use-cases and concerns. With that, if you'd like to discuss this issue further, please feel free to either schedule a call directly on my Calendly link, or email me at korinne@apollographql.com. Thanks in advance!

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