How to handle overrides when importing?
Opened this issue Β· 24 comments
Firstly, I have an example reproduction of the issue I'm facing available here: https://github.com/Siyfion/node-graphql-starter/tree/graphql-import
So what I'm trying to do, is get my Prisma generated schema imported into my Apollo Server 2.0 project. It works fine doing it the way I am, until I re-define a type that's indirectly being imported with graphql-import
.
Clearly I need a way of merging the schema and when encountering duplicates, using the ones defined in my code over the imported ones, but I can't seem to figure out a way to do this?
FYI the error in the example repo is: Error: Type "User" was defined more than once.
This is due to me having defined a custom type for User that adds a fullname
field to the original schema.
Can you still reproduce this with graphql-yoga
and/or apollo-server@1.x
?
@marktani I'm not 100% certain if I can or can't... One of the reasons for me moving to an Apollo Server 2.0 was to better organise my code, create a more modular based structure. I've had it working in graphql-yoga
but only when I've had my import
and entire schema all in one massive *.graphql
file.
So as a bit of extra information, it all seems to work fine if I do the following:
- Remove the type definitions from
user.js
andvenue.js
- Add the type definitions to
imports.graphql
Resulting in:
# import List from './generated/prisma.graphql'
# import ListItem from './generated/prisma.graphql'
# import Menu from './generated/prisma.graphql'
# import OpeningHours from './generated/prisma.graphql'
# import Tag from './generated/prisma.graphql'
# import VenueWhereInput, VenueOrderByInput from './generated/prisma.graphql'
# import Zone from './generated/prisma.graphql'
type User {
id: ID!
activities: [ActivityEvent!]!
areas: [Area!]
email: String
firstName: String
following: [User!]
followedLists: [List!]
fullName: String
username: String
lastName: String
lists: [List!]
location: String
metadata: Json
profileImage: String
tags: [Tag!]!
zones: [Zone!]!
}
type Venue {
id: ID!
name: String!
shortDescription: String!
longDescription: String!
images: [String!]!
latitude: Float!
longitude: Float!
addressLine1: String!
addressLine2: String!
addressCity: String!
postcode: String!
zone: Zone!
tags: [Tag!]!
contactNumber: String!
pdfMenus: [Menu!]!
lists: [List!]!
listItems: [ListItem!]!
instagramHandle: String!
bookingUrl: String!
bookingType: String!
wifiSSID: String!
wifiPassword: String!
openingHours: OpeningHours!
published: Boolean!
}
But, this is the key bit... This exactly what I was trying to avoid. I hate having to define all my types in one big *.graphql
file, or even in a multitude of small files dotted around. I much prefer the way that the files are structured in my branch above.
I think that there is some magic going on with importSchema
whereby it either doesn't import types that are defined locally, or simply overwrites the imported types with the manually defined ones?
Clearly that won't work with the way the code is structured (importSchema
knows nothing about what is defined locally, as it's only merged in later), but there must be a way to "merge" them, keeping the user-defined types over any imported ones?
I added a test case for collision in GraphQL import.
That part looks to be working as expected.
But I think you want to merge imported type with type defined in JS/TS. Let me get back to you on this.
@divyenduz yes, that's precisely my use-case. It's all about how to combine the use of graphql-import
with types defined in JS/TS using the gql
decorator. The merge happens here, but then there's no conflict-resolution
I appreciate it's not really a graphql-import
issue, but as I think I mentioned in our chat on Slack, it would be really good to have a known working solution for this use-case!
@Siyfion : Noted, this can happen via some additional tooling exposed by graphql-import
or a separate tooling altogether.
I remember that you tried mergeSchemas
to resolve this. What was the outcome?
If I'm completely honest, I got a little worried when it started trying to merge two different root Query
objects, it's quite possible that this is the correct solution to go down, though I fear my lack of knowledge of the internal workings of the GraphQL AST may have scared me off! π As I didn't fully understand what it was doing.
@marktani & others
After a lot of discussion & help from @divyenduz, I think I've got somewhere close to a workable solution! In short, if you want to keep your GraphQL in JS and modular, you need to convert it back to SDL, merge it all together, add any # import
statements and then run it through graphql-import
, like so:
// Convert the typeDefs BACK to SDL from objects
const schemaSDLArray = typeDefs.map(typeDef => print(typeDef))
// Push the imports as a simple string to the top of the array
schemaSDLArray.unshift(imports)
// Merge the array into one big SDL "file"
const schemaSDL = schemaSDLArray.join('\n')
// Run the SDL through importSchema, so that the dependencies are imported
const withImportsSDL = importSchema(schemaSDL)
I've updated my branch to now have the new code in it, with the main commit being: https://github.com/Siyfion/node-graphql-starter/commit/7adaca411161a1e93bf356a8de051d50e2bd163c
However, I have uncovered an issue with graphql-import
as a result; all occurrences of extend type ...
in the SDL are being removed post-import. :/
The issue with extend type ...
seems related to #42 however, I think that issue is dealing with a scenario whereby the original type and the extension are in separate files. In this case, both the type and the extension are all in the one SDL block. Type extensions are now part of the spec (http://facebook.github.io/graphql/June2018/#sec-Object-Extensions).
This is now a blocker to getting this issue resolved unfortunately.
@divyenduz I actually donβt think that the "supportβ Iβm requiring should be a very big change at allβ¦ Perhaps it wasnβt fully explained? I donβt need graphql-import
to support extend type X
statements in different files, and handle the imports. All I need it to do is not wipe out extend type
from the βrootβ file being imported. eg:
# import Author from "./someImport.graphql"
type Story {
name: String!
}
extend type Story {
author: Author!
}
at the moment the extend
just gets ignored and removed from the output, if this file is run through graphql-import
.
The extend type
code is all handled nicely by Apollo Server v2, so I really just need the import to leave that statement alone and not delete it!
hey @Siyfion
I had the same issue and used a more "manual" method to import types that I need:
import * as fs from 'fs'
import * as path from 'path'
import { gql } from 'apollo-server-express'
const prismaDefs = gql(
fs.readFileSync(
path.resolve(path.join(__dirname), '../generated/prisma.graphql'),
'utf8',
),
)
export const importPrismaDefs = (namesToImport: string[]) => {
const definitions: any[] = prismaDefs.definitions
.filter((definition: any) => {
if (definition.name) {
return namesToImport.includes(definition.name.value)
}
return false
})
return {
...prismaDefs,
definitions,
}
}
I used this function to import all "inputs" that I didn't want to recreate like UserCreateInput
or UserWhereInput
(especially with all "where"-style inputs which can have dozens and dozens of fields).
It forced me to list all necessary types (around forty), but still I migrate from Yoga to Apollo-server v2, I was normally able to extend them.
So I use you logic which works well!
But from your differents tests, did you succeed to find a compromise to allow to use extend
type?
Hey @johannpinson
Thats a pretty neat way to do it! I haven't found a way to use extend
with types defined in the "imports". However, I can use extend
internally.
How does your strategy cope with "overrides"? I guess that as you are importing things entirely manually, you just don't import them and then redefine them in your schema?
Are you using GraphQL in JS (gql
tags) or *.graphql
files in your Apollo Server v2 project? It seems as though they (and I) prefer using a GraphQL in JS approach!
Finally I also use extend
logic by this way:
const baseTypes = gql`
# Original queries, mutations, types and input that I need for my server
...
`
const extendedTypes = gql`
extend type User {
test: String
}
`
-----
import { baseTypes, extendedTypes } from './types'
// Convert the typeDefs BACK to SDL from objects
const schemaSDLArray: string[] = [print(baseTypes)]
// Push the imports as a simple string to the top of the array
schemaSDLArray.unshift(imports)
// Merge the array into one big SDL "file"
const schemaSDL: string = schemaSDLArray.join('\n')
// Run the SDL through importSchema, so that the dependencies are imported
const baseTypeDefs: DocumentNode = gql(importSchema(schemaSDL))
// Merge all differents types
const typeDefs = [baseTypeDefs, extendedTypes]
And it looks to work π(and yes I know, the unshift
is useless ^^).
Like you see, I use too the apollo-tag (aka gql
) since I switched to Apollo-server v2.
It allow me to make some ${custom}
code inside the tag π
(well, vscode-graghql -- hey @divyenduz π -- doesn't highlight it well, but I'm sure I will need it more and more in the future).
By the way, the extends that I use a really simple for the moment like add a field into a type or an input.
I doesn't have yet a case when it will need to update a more, more than just "add" it.
In fact, it will be also the unextend
key which I dream π
That code looks familiar @johannpinson! π
I may well have a play with your approach later on tonight, or early next week. I feel like you might have cracked the final piece of the puzzle! π
Do you have a repo with all this as a working example?
Ha ah, sure @Siyfion!
I said before I moved into your logic when I discover it earlier this week π
After I not sure it the "perfect" final piece, because we use gql
, transform it in string
and re-parse it in gql
π but for the moment it works.
The only example I have it the project on which I work for a client, so I will try to create a demo asap (because I will be on vacation few days from tomorrow).
Hey @johannpinson I've been trying things out with your method, and I've got it all working...
The good news is that your block:
// Convert the typeDefs BACK to SDL from objects
const schemaSDLArray: string[] = [print(baseTypes)]
// Push the imports as a simple string to the top of the array
schemaSDLArray.unshift(imports)
// Merge the array into one big SDL "file"
const schemaSDL: string = schemaSDLArray.join('\n')
// Run the SDL through importSchema, so that the dependencies are imported
const baseTypeDefs: DocumentNode = gql(importSchema(schemaSDL))
// Merge all differents types
const typeDefs = [baseTypeDefs, extendedTypes]
no longer needs to exist! You just need to merge all your arrays of typeDefs
into one large array that you pass into the ApolloServer
constructor.
Latest (working) code now available here:
https://github.com/Siyfion/node-graphql-starter/tree/graphql-import
Hello @Siyfion
Yes the solution that I gave works, but it requires you write any type you need inside the imports.js
.
It's why I prefer your version with graphql-import
, because the package auto-imports the type/object that you can need without declare all of them.
After it depends how you want to manage the imports of external types :
- auto-import default and missing type with
graphql-import
, so no worries about forget one - manual import of what you need, which allow you to see each imported object precisely
One perfect solution which be that graphql-import
auto-add a comment on types imported and so we will can see this flag inside GraphQL Playground for example π
Yeah, I prefer the explicit import syntax, as if your GraphQL Playground endpoint is exposed publicly, they don't get to see the internal workings of your entire DB structure, which the automatically imported Prisma types/imports, etc. all give away a bit!
Yes I see but it forces me to have all this import for one type created for example:
const TypeImportedFromPrisma = importPrismaDefs([
// Top-level requested type by previous declarations
'SpaceCreateInput',
'SpaceUpdateInput',
'SpaceWhereUniqueInput',
// Inherit requested type
// For 'SpaceCreateInput'
'StoryCreateManyWithoutSpaceInput',
'StoryCreateWithoutSpaceInput',
'StoryWhereUniqueInput',
'StoryCreateimagesInput',
'UserCreateOneWithoutSpacesInput',
'UserCreateWithoutSpacesInput',
'UserWhereUniqueInput',
// For 'SpaceUpdateInput'
'StoryUpdateManyWithoutSpaceInput',
'StoryUpdateWithWhereUniqueWithoutSpaceInput',
'StoryUpsertWithWhereUniqueWithoutSpaceInput',
'StoryUpdateWithoutSpaceDataInput',
'StoryUpdateimagesInput',
'UserUpdateOneWithoutSpacesInput',
'UserUpdateWithoutSpacesDataInput',
'UserUpsertWithoutSpacesInput',
])
and my final model will have around a twenty type which can have relation between them π
True, I could argue that exposing create/update/upsert
input types is very dangerous if you have any kind of permissions model, but I'm guessing you know that!?
We don't expose any Prisma types for any mutations. They don't allow us to give the level of control that we needed.
Yes all mutations (and some queries/fields) will be cover by graphql-shield
+ graphql-middleware
for custom checks.
I'm testing also the Schema directives as solution, but the first permission layer will be implemented in front.
So to simulate the JWT Auth + context of request will be a good challenge to force the security ^^
On your side, you will "rewrite" all input and remove "connect"-like field available from Prisma?
Few months ago I got so frustrated with exactly all this, that I made a tool. Weird that I did not notice this issue before.
Frankly - right now I'm more partial to interpolated string literals in ts, but I'm plugging this as some curiosity: https://github.com/vadistic/graphql-override
It even integrates with prisma config (and works)