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

Work with multiple projects and/or support GraphQL Config #247

Open
Cellule opened this issue Dec 12, 2024 · 7 comments
Open

Work with multiple projects and/or support GraphQL Config #247

Cellule opened this issue Dec 12, 2024 · 7 comments

Comments

@Cellule
Copy link

Cellule commented Dec 12, 2024

In our project, we're working with multiple different graphs.
Sometimes these graphs are actually external services we do not control but we write queries to target them.
We've been using VS Code GraphQL: Language Feature Support extension for GraphQL LSP

I've been setting up GraphQL Config https://the-guild.dev/graphql/config to tell the VS Code extension which files should use which schema

Here's the config we are currently using

graphql.config.js
const mxSchema = "backend/packages/graphql-schema/schema.gen.graphql";

module.exports = {
  projects: {
    default: {
      schema: mxSchema,
    },
    "backend-tests": {
      schema: mxSchema,
      documents: ["backend/api/src/**/__tests__/**/*.spec.ts"],
    },
    "server-rendering": {
      schema: mxSchema,
      documents: [
        "backend/server-rendering/src/graphql/**/*!(.gen).{ts,tsx}",
        "backend/server-rendering/src/modules/**/graphql/**/*!(.gen).{ts,tsx}",
      ],
    },
    "work-order-asset-snapshots": {
      schema: mxSchema,
      documents: ["backend/graphql/src/mutations/helpers/workOrderAssetSnapshot/**/*.{ts,tsx}"],
    },
    frontend: {
      schema: mxSchema,
      documents: [
        "frontend/src/**/*.gql!(.gen).{ts,tsx}",
        "frontend/src/graphql/**/*!(.gen).{ts,tsx}",
        "frontend/src/modules/**/graphql/**/*!(.gen).{ts,tsx}",
        "native/src/shared-all/graphql/**/*!(.gen).{ts,tsx}",
        "native/src/shared-clients/graphql/**/*!(.gen).{ts,tsx}",
      ],
    },
    dashboard: {
      schema: mxSchema,
      documents: [
        "dashboard/src/**/*.gql!(.gen).{ts,tsx}",
        "dashboard/src/graphql/**/*!(.gen).{ts,tsx}",
        "dashboard/src/modules/**/graphql/**/*!(.gen).{ts,tsx}",
        "native/src/shared-all/graphql/**/*!(.gen).{ts,tsx}",
        "native/src/shared-clients/graphql/**/*!(.gen).{ts,tsx}",
      ],
    },
    native: {
      schema: mxSchema,
      documents: [
        "native/src/**/*.gql!(.gen).{ts,tsx}",
        "native/src/graphql/**/*!(.gen).{ts,tsx}",
        "native/src/modules/**/graphql/**/*!(.gen).{ts,tsx}",
        "native/src/shared-all/graphql/**/*!(.gen).{ts,tsx}",
        "native/src/shared-clients/graphql/**/*!(.gen).{ts,tsx}",
      ],
    },
    gpl: {
      schema: "gql-gen/gpl.gen.graphql",
      documents: "backend/common/src/core/gpl/*!(.gen).ts",
    },
    metal: {
      schema: "backend/node_modules/@metal/graphql/schema.gen.graphql",
      documents: "backend/common/src/core/metal/*!(.gen).ts",
    },
  },
};

You'll notice that we have multiple "client" applications targeting the same schema which allows to better understand which fragments belongs to which project.
Also at the end you'll notice that have a local copy of the gpl project that is an external api we call and the metal project has an sdk providing the schema that we're using as well.

Worth noting that we have a separate config for GraphQL Codegen, because the VS Code GraphQL LSP extension doesn't support multiple schema files even though the GraphQL Config standard supports it. So we stitch all our schemas into 1 big schema file and pipe this into the extension instead

Since we are starting to look into Federation, we are investigating better extension to empower our devs and was hoping Apollo's extension would support the GraphQL Config for a more standardized way to provide a great GraphQL experience to our developpers.

@Cellule
Copy link
Author

Cellule commented Dec 12, 2024

I tried the following to at least get support for the main project

const graphqlConfig = require("./graphql.config");

const defaultSchema = graphqlConfig.projects.default.schema;

module.exports = {
  client: {
    service: {
      // can be a string pointing to a single file or an array of strings
      localSchemaFile: defaultSchema,
    },
    includes: Array.from(
      new Set(
        Array.from(
          new Set(
            // Object.values(graphqlConfig.projects)
           // Somehow native and frontend are causing the extension to crash
            [
              graphqlConfig.projects.dashboard,
              graphqlConfig.projects["server-rendering"],
              graphqlConfig.projects["work-order-asset-snapshots"],
            ]
              .filter((p) => p.schema === defaultSchema)
              .flatMap((p) => p.documents || []),
          ),
        ),
      ),
    ),
    excludes: ["*.gen.*"],
  },
};

Not sure why, but if I include files from the frontend or native projects the extension crashes with this error

/Users/micfer/.vscode/extensions/apollographql.vscode-apollo-2.5.1/lib/language-server/server.js:1084
`)}t(Dor,"dedentBlockStringValue");function kor(i){let r=null;for(let s=1;s<i.length;s++){let c=i[s],u=Mqt(c);if(u!==c.length&&(r===null||u<r)&&(r=u,r===0))break}return r===null?0:r}t(kor,"getBlockStringIndentation");function Mqt(i){let r=0;for(;r<i.length&&(i[r]===" "||i[r]==="	");)r++;return r}t(Mqt,"leadingWhitespace");function Rqt(i){return Mqt(i)===i.length}t(Rqt,"isBlank");var Qqt=jo(Fo(),1);function jqt(i){return i&&typeof i=="object"&&"kind"in i&&i.kind===Qqt.Kind.DOCUMENT}t(jqt,"isDocumentNode");function Uqt(i,r,s){let c=xor([...r,...i].filter(yz),s);return s&&s.sort&&c.sort(sM),c}t(Uqt,"mergeArguments");function xor(i,r){return i.reduce((s,c)=>{let u=s.findIndex(_=>_.name.value===c.name.value);return u===-1?s.concat([c]):(r?.reverseArguments||(s[u]=c),s)},[])}t(xor,"deduplicateArguments");function wor(i,r){return!!i.find(s=>s.name.value===r.name.value)}t(wor,"directiveAlreadyExists");function qqt(i,r){return!!r?.[i.name.value]?.repeatable}t(qqt,"isRepeatableDirective");function Vqt(i,r){return r.some(({value:s})=>s===i.value)}t(Vqt,"nameAlreadyExists");function Jqt(i,r){let s=[...r];for(let c of i){let u=s.findIndex(_=>_.name.value===c.name.value);if(u>-1){let _=s[u];if(_.value.kind==="ListValue"){let g=_.value.values,b=c.value.values;_.value.values=Wqt(g,b,(I,w)=>{let L=I.value;return!L||!w.some(V=>V.value===L)})}else _.value=c.value}else s.push(c)}return s}t(Jqt,"mergeArguments");function Nor(i,r){return i.map((s,c,u)=>{let _=u.findIndex(g=>g.name.value===s.name.value);if(_!==c&&!qqt(s,r)){let g=u[_];return s.arguments=Jqt(s.arguments,g.arguments),null}return s}).filter(yz)}t(Nor,"deduplicateDirectives");function iv(i=[],r=[],s,c){let u=s&&s.reverseDirectives,_=u?i:r,g=u?r:i,b=Nor([..._],c);for(let I of g)if(wor(b,I)&&!qqt(I,c)){let w=b.findIndex(V=>V.name.value===I.name.value),L=b[w];b[w].arguments=Jqt(I.arguments||[],L.arguments||[])}else b.push(I);return b}t(iv,"mergeDirectives");function Gqt(i,r){return r?{...i,arguments:Wqt(r.arguments||[],i.arguments||[],(s,c)=>!Vqt(s.name,c.map(u=>u.name))),locations:[...r.locations,...i.locations.filter(s=>!Vqt(s,r.locations))]}:i}t(Gqt,"mergeDirective");function Wqt(i,r,s){return i.concat(r.filter(c=>s(c,i)))}t(Wqt,"deduplicateLists");function Hqt(i,r,s,c){if(s?.consistentEnumMerge){let g=[];i&&g.push(...i),i=r,r=g}let u=new Map;if(i)for(let g of i)u.set(g.name.value,g);if(r)for(let g of r){let b=g.name.value;if(u.has(b)){let I=u.get(b);I.description=g.description||I.description,I.directives=iv(g.directives,I.directives,c)}else u.set(b,g)}let _=[...u.values()];return s&&s.sort&&_.sort(sM),_}t(Hqt,"mergeEnumValues");var zqt=jo(Fo(),1);function $qt(i,r,s,c){return r?{name:i.name,description:i.description||r.description,kind:s?.convertExtensions||i.kind==="EnumTypeDefinition"||r.kind==="EnumTypeDefinition"?"EnumTypeDefinition":"EnumTypeExtension",loc:i.loc,directives:iv(i.directives,r.directives,s,c),values:Hqt(i.values,r.values,s)}:s?.convertExtensions?{...i,kind:zqt.Kind.ENUM_TYPE_DEFINITION}:i}t($qt,"mergeEnum");var d9=jo(Fo(),1);function Yqt(i){return typeof i=="string"}t(Yqt,"isStringTypes");function Kqt(i){return i instanceof d9.Source}t(Kqt,"isSourceTypes");function hXe(i){let r=i;for(;r.kind===d9.Kind.LIST_TYPE||r.kind==="NonNullType";)r=r.type;return r}t(hXe,"extractType");function gXe(i){return i.kind!==d9.Kind.NAMED_TYPE}t(gXe,"isWrappingTypeNode");function FCe(i){return i.kind===d9.Kind.LIST_TYPE}t(FCe,"isListTypeNode");function lM(i){return i.kind===d9.Kind.NON_NULL_TYPE}t(lM,"isNonNullTypeNode");function Lae(i){return FCe(i)?`[${Lae(i.type)}]`:lM(i)?`${Lae(i.type)}!`:i.name.value}t(Lae,"printTypeNode");var cM;(function(i){i[i.A_SMALLER_THAN_B=-1]="A_SMALLER_THAN_B",i[i.A_EQUALS_B=0]="A_EQUALS_B",i[i.A_GREATER_THAN_B=1]="A_GREATER_THAN_B"})(cM||(cM={}));function Xqt(i,r){return i==null&&r==null?cM.A_EQUALS_B:i==null?cM.A_SMALLER_THAN_B:r==null?cM.A_GREATER_THAN_B:i<r?cM.A_SMALLER_THAN_B:i>r?cM.A_GREATER_THAN_B:cM.A_EQUALS_B}t(Xqt,"defaultStringComparator");function Por(i,r){let s=i.findIndex(c=>c.name.value===r.name.value);return[s>-1?i[s]:null,s]}t(Por,"fieldAlreadyExists");function Ez(i,r,s,c,u){let _=[];if(s!=null&&_.push(...s),r!=null)for(let g of r){let[b,I]=Por(_,g);if(b&&!c?.ignoreFieldConflicts){let w=c?.onFieldTypeConflict&&c.onFieldTypeConflict(b,g,i,c?.throwOnConflict)||Oor(i,b,g,c?.throwOnConflict);w.arguments=Uqt(g.arguments||[],b.arguments||[],c),w.directives=iv(g.directives,b.directives,c,u),w.description=g.description||b.description,_[I]=w}else _.push(g)}if(c&&c.sort&&_.sort(sM),c&&c.exclusions){let g=c.exclusions;return _.filter(b=>!g.includes(`${i.name.value}.${b.name.value}`))}return _}t(Ez,"mergeFields");function Oor(i,r,s,c=!1){let u=Lae(r.type),_=Lae(s.type);if(u!==_){let g=hXe(r.type),b=hXe(s.type);if(g.name.value!==b.name.value)throw new Error(`Field "${s.name.value}" already defined with a different type. Declared as "${g.name.value}", but you tried to override with "${b.name.value}"`);if(!Mae(r.type,s.type,!c))throw new Error(`Field '${i.name.value}.${r.name.value}' changed type from '${u}' to '${_}'`)}return lM(s.type)&&!lM(r.type)&&(r.type=s.type),r}t(Oor,"preventConflicts");function Mae(i,r,s=!1){if(!gXe(i)&&!gXe(r))return i.toString()===r.toString();if(lM(r)){let c=lM(i)?i.type:i;return Mae(c,r.type)}return lM(i)?Mae(r,i,s):FCe(i)?FCe(r)&&Mae(i.type,r.type)||lM(r)&&Mae(i,r.type):!1}t(Mae,"safeChangeForFieldType");var Zqt=jo(Fo(),1);function eJt(i,r,s,c){if(r)try{return{name:i.name,description:i.description||r.description,kind:s?.convertExtensions||i.kind==="InputObjectTypeDefinition"||r.kind==="InputObjectTypeDefinition"?"InputObjectTypeDefinition":"InputObjectTypeExtension",loc:i.loc,fields:Ez(i,i.fields,r.fields,s),directives:iv(i.directives,r.directives,s,c)}}catch(u){throw new Error(`Unable to merge GraphQL input type "${i.name.value}": ${u.message}`)}return s?.convertExtensions?{...i,kind:Zqt.Kind.INPUT_OBJECT_TYPE_DEFINITION}:i}t(eJt,"mergeInputType");var tJt=jo(Fo(),1);function For(i,r){return!!i.find(s=>s.name.value===r.name.value)}t(For,"alreadyExists");function Sz(i=[],r=[],s={}){let c=[...r,...i.filter(u=>!For(r,u))];return s&&s.sort&&c.sort(sM),c}t(Sz,"mergeNamedTypeArray");function nJt(i,r,s,c){if(r)try{return{name:i.name,description:i.description||r.description,kind:s?.convertExtensions||i.kind==="InterfaceTypeDefinition"||r.kind==="InterfaceTypeDefinition"?"InterfaceTypeDefinition":"InterfaceTypeExtension",loc:i.loc,fields:Ez(i,i.fields,r.fields,s,c),directives:iv(i.directives,r.directives,s,c),interfaces:i.interfaces?Sz(i.interfaces,r.interfaces,s):void 0}}catch(u){throw new Error(`Unable to merge GraphQL interface "${i.name.value}": ${u.message}`)}return s?.convertExtensions?{...i,kind:tJt.Kind.INTERFACE_TYPE_DEFINITION}:i}t(nJt,"mergeInterface");var cb=jo(Fo(),1),lJt=jo(dx(),1);var rJt=jo(Fo(),1);function iJt(i,r,s,c){return r?{name:i.name,description:i.description||r.description,kind:s?.convertExtensions||i.kind==="ScalarTypeDefinition"||r.kind==="ScalarTypeDefinition"?"ScalarTypeDefinition":"ScalarTypeExtension",loc:i.loc,directives:iv(i.directives,r.directives,s,c)}:s?.convertExtensions?{...i,kind:rJt.Kind.SCALAR_TYPE_DEFINITION}:i}t(iJt,"mergeScalar");var Tz=jo(Fo(),1);var RCe={query:"Query",mutation:"Mutation",subscription:"Subscription"};function Ror(i=[],r=[]){let s=[];for(let c in RCe){let u=i.find(_=>_.operation===c)||r.find(_=>_.operation===c);u&&s.push(u)}return s}t(Ror,"mergeOperationTypes");function sJt(i,r,s,c){return r?{kind:i.kind===Tz.Kind.SCHEMA_DEFINITION||r.kind===Tz.Kind.SCHEMA_DEFINITION?Tz.Kind.SCHEMA_DEFINITION:Tz.Kind.SCHEMA_EXTENSION,description:i.description||r.description,directives:iv(i.directives,r.directives,s,c),operationTypes:Ror(i.operationTypes,r.operationTypes)}:s?.convertExtensions?{...i,kind:Tz.Kind.SCHEMA_DEFINITION}:i}t(sJt,"mergeSchemaDefs");var oJt=jo(Fo(),1);function aJt(i,r,s,c){if(r)try{return{name:i.name,description:i.description||r.description,kind:s?.convertExtensions||i.kind==="ObjectTypeDefinition"||r.kind==="ObjectTypeDefinition"?"ObjectTypeDefinition":"ObjectTypeExtension",l[Error - 12:02:36 PM] Server process exited with code 1.
[Info  - 12:02:36 PM] Connection to server got closed. Server will restart.
true

@phryneas
Copy link
Member

For what it's worth, right now you can have multiple configurations in multiple folders, and it should find all of those.

As for that crash: do you get any more information than that? Could you rename the config file to end in .cjs to ensure it's evaluated as commonJs?

@Cellule
Copy link
Author

Cellule commented Dec 16, 2024

I didn't know you could have apollo.config.js in multiple folders, I'm gonna try that right now.

As for the error, I cloned this repo and launch the extension in debug and I was able to get the actual error message
It seems it crashes on conflicting operations which happens because I was grouping too much stuff together
It also happens in a single project where some devs copy pasted some operations and happen to be exactly the same (otherwise GraphQL-Codegen would have complained). I'll have to make this rule stricter
That being said, it would be nice if the extension could properly report the error in production build and even better to not crash and actually report the error in the IDE

Debugger listening on ws://127.0.0.1:6009/53ca72a0-f97b-4386-99ff-3235f60a6756
For help, see: https://nodejs.org/en/docs/inspector
/Users/micfer/projects/vscode-graphql/lib/language-server/server.js:324152
                  throw new Error(
                  ^

Error: ️️There are multiple definitions for the `WorkOrderSnapshotQuery` operation. Please fix all naming conflicts before continuing.
Conflicting definitions found at /Users/micfer/projects/maintainx/frontend/src/graphql/queries/WorkOrderDetailsSnapshotQuery.ts and /Users/micfer/projects/maintainx/backend/graphql/src/mutations/helpers/workOrderAssetSnapshot/queries/workOrderSnapshotQuery.ts.
    at GraphQLClientProject.<anonymous> (/Users/micfer/projects/vscode-graphql/lib/language-server/server.js:324152:25)
    at invokeFunc (/Users/micfer/projects/vscode-graphql/lib/language-server/server.js:307137:23)
    at trailingEdge (/Users/micfer/projects/vscode-graphql/lib/language-server/server.js:307168:18)
    at Timeout.timerExpired [as _onTimeout] (/Users/micfer/projects/vscode-graphql/lib/language-server/server.js:307160:18)
    at listOnTimeout (node:internal/timers:581:17)
    at process.processTimers (node:internal/timers:519:7)

Node.js v20.16.0
[Error - 3:42:59 PM] Server process exited with code 1.
[Error - 3:42:59 PM] The Apollo GraphQL server crashed 5 times in the last 3 minutes. The server will not be restarted. See the output for more information.

@Cellule
Copy link
Author

Cellule commented Dec 17, 2024

Alright as for multi repos I got it working with multiple apollo.config.js files

Now I'm facing another issue where the extension doesn't seem to pick up my local schema files in the include.
It works when I'm working on operations great! But when I'm editing the schema locally, I don't have basic go to definition features working.

Since my original issue is solved do you want me to open different issues for this and the duplicate definition error ?

@phryneas
Copy link
Member

phryneas commented Dec 17, 2024

Yeah, let's keep this in a bunch of separate issues.

That said, don't expect too many "schema editing" functionality - what you are creating here are client projects, so the focus is on client development - it mostly just reads the schema in and uses that as a starting point. From the back of my mind, I'm not sure how many features we have enabled in schemas.

For schema development, you would create a rover project - those even have federation and connectors support, but the functionality is currently still a preview feature:

https://www.apollographql.com/docs/graphos/schema-design/connectors/vs-code

@Cellule
Copy link
Author

Cellule commented Dec 17, 2024

Worth mentioning that in 1 project I need to call into multiple different graphql services
I initially created apollo.config.mjs files into the various folders hosting the various operations.
But those were in sources so TypeScript started picking up apollo.config.mjs file (since we use allowJs)

I had to move the various apollo.config.mjs files to the root of the project in a folder structure like so

- backend
  - apollo-configs
    - metal
      - apollo.config.mjs
    - gql
      - apollo.config.mjs
    - snapshots
      - apollo.config.mjs
  - src
  	- metal
  	- gpl
  	- snapshots

This feels a bit convoluted and I'm not sure why I couldn't put all my graphql projects into 1 apollo.config file considering the extension does support multiple configs through discoverability
The graphql.config.js file I put at the top is much simpler to manage in a monorepo

@Cellule
Copy link
Author

Cellule commented Dec 17, 2024

In the end the work around to have multiple apollo.config.js files worked.
I'd still prefer some way to define everything in 1 file
Also would be nice to document how to work with multiple projects somewhere (unless I missed it?)

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

2 participants