spectaql
spectaql copied to clipboard
Add support for grouping Queries or Mutations
I am working with Graphene Django. I wanted to group my queries and mutations based on some criteria. It would be a plus If we could get subgrouping in Query and Mutation Groups.
Hi @haris1998-web
It is possible to customize the organization of the data that's rendered (including the Query and Mutation groups) by creating your own theme and overriding the ./data/index.js file to arrange your Queries and Mutations the way you like.
Here is an example customized data/index.js in a theme: https://github.com/anvilco/spectaql/blob/main/examples/themes/my-partial-theme/data/index.js
And here is the data/index.js that is used in SpectaQL's default theme: https://github.com/anvilco/spectaql/blob/main/src/themes/default/data/index.js
This blog post or this area of our documentation should help get you going, but it is definitely possible by creating a custom theme with just a single file in it.
@haris1998-web did you have any success with that?
Grouping queries, mutations and types would be very useful, but I wouldn't know how to use the theme to implement this either. An example with metadata would be helpful.
Related to https://github.com/anvilco/spectaql/issues/442
Currently you'd need to know a bit about coding and implement your desired behavior via a custom theme data arranger.
Perhaps I could add support for a metadata option that can indicate how you'd like to group/arrange things. Then users would not need to know how to code necessarily, nor need to make a theme, but simply get the right metadata applied.
Perhaps I could add support for a metadata option that can indicate how you'd like to group/arrange things
That would be wonderful. We have a complicated set of GraphQL queries and mutations and I'd like to see them organised into themes like account management, device management, etc. I think you're saying above that I need to implement this by overriding the ./data/index.js file but it'd be ideal if this could be via tags in the metadata.
@newhouse let me know if below is too long / you want it deleted.
Hope this saves the next person from all the time it took me :)
Below is what I wrote to address this issue, to space out our docs for our public facing Graphql api.
- I'm a
rubydeveloper so place excuse myjsskills - you'll need
lodash / microfibrein your project. genericServiceGeneratordoes most of the heavy lifting
End result to reverse engineer https://graphql-docs.getsubmarine.com/
// data/index.js
Object.defineProperty(exports, '__esModule', { value: true });
exports.default = void 0;
const renderedObjectsArray = []
const objectsExcludeArray = ["Connection", "Edge", "Payload"]
const connectionArray = ["Connection", "Edge"]
const coreStore =
// returned objects are auto added at runtime using
{
Channels: ["channel", "channels"]
}
const notificationsStore =
// returned objects are auto added at runtime
{
Webhooks: ["webhook", "webhooks", "webhookCreate", "webhookDelete", "webhookUpdate"]
}
const paymentsStore =
// returned objects are auto added at runtime
{
Charges: ["charge", "charges"]
}
const presalesStore =
// returned objects are auto added at runtime
{
Campaign_Orders: ["campaignOrder", "campaignOrderPagination", "campaignOrders"]
}
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const _sortBy = _interopRequireDefault(require('lodash/sortBy'));
const { groupBy } = require('lodash')
const { Microfiber: IntrospectionManipulator } = require('microfiber');
module.exports = ({
// The Introspection Query Response after all the augmentation and metadata directives
// have been applied to it
introspectionResponse,
// All the options that are specifically for the introspection related behaviors, such a this area.
introspectionOptions,
// A GraphQLSchema instance that was constructed from the provided introspectionResponse
graphQLSchema: _graphQLSchema,
// All of the SpectaQL options in case you need them for something.
allOptions: _allOptions,
}) => {
const introspectionManipulator = new IntrospectionManipulator(
introspectionResponse,
// microfiberOptions come from converting some of the introspection options to a corresponding
// option for microfiber via the `src/index.js:introspectionOptionsToMicrofiberOptions()` function
introspectionOptions?.microfiberOptions
);
const queryType = introspectionManipulator.getQueryType();
const mutationType = introspectionManipulator.getMutationType()
const normalTypes = introspectionManipulator.getAllTypes({
includeQuery: false,
includeMutation: false,
includeSubscription: false,
});
const groupedOtherTypes = groupBy(normalTypes, (thing) => {
return thing.kind
})
const groupedObjects = groupedOtherTypes.OBJECT
const enumTypes = groupedOtherTypes.ENUM
const scalarTypes = groupedOtherTypes.SCALAR
const inputObjects = groupedOtherTypes.INPUT_OBJECT;
const unionTypes = groupedOtherTypes.UNION
// groupedObjects.push(...unionObjects);
const hasTypeKind = (typeKind, storeArray, key) => {
if (typeKind.fields) {
return typeKind.fields.map(query => (query.name)).some(r => storeArray[key].includes(r))
} else {
return typeKind.map(query => (query.name)).some(r => storeArray[key].includes(r))
}
}
const addToStoreFilter = (type, typeArray) => {
if (typeArray.includes(type.name) && !type["type"]["name"].includes("Connection")) {
renderedObjectsArray.push(type["type"]["name"])
typeArray.push(type["type"]["name"])
}
if (type.name) {
return typeArray.includes(type.name)
}
}
const genericServiceGenerator = (service, typeStore) => {
return {
name: service, // Main Heading
hideInNav: false,
hideInContent: true,
makeNavSection: false,
makeContentSection: false,
items: Object.entries(typeStore)
.map(([keyType, typeArray]) => {
return {
hideInNav: false,
hideInContent: false,
makeNavSection: true,
makeContentSection: true,
name: keyType.replace(/_/g, " "), // store Keys
items: [
// Queries
hasTypeKind(queryType, typeStore, keyType) ?
{
name: 'Queries',
makeNavSection: true,
makeContentSection: true,
items: (0, _sortBy.default)(queryType.fields
.map((query) => ({
...query,
isQuery: true,
}))
.filter((type) => addToStoreFilter(type, typeArray)),
'name'),
} : null,
// Mutations
hasTypeKind(mutationType, typeStore, keyType) ?
{
name: 'Mutations',
makeNavSection: true,
makeContentSection: true,
items: (0, _sortBy.default)(
mutationType.fields.map((query) => ({
...query,
isMutation: true,
}))
.filter(type => typeArray.includes(type.name)),
'name'
),
} : null,
// normal Types
hasTypeKind(normalTypes, typeStore, keyType) ?
{
name: 'Objects',
makeContentSection: true,
makeNavSection: true,
items: (0, _sortBy.default)(
normalTypes.map((type) => ({
...type,
isType: true,
})).filter(type => typeArray.includes(type.name)),
'name'
),
} : null]
}
}
)
}
}
return [
genericServiceGenerator("Core", coreStore),
genericServiceGenerator("Notifications", notificationsStore),
genericServiceGenerator("Payments", paymentsStore),
genericServiceGenerator("Presales", presalesStore),
// *** Graph Ql Types *** /
//
{
name: 'GraphQl Types',
hideInNav: false,
hideInContent: true,
makeNavSection: false,
makeContentSection: false,
items: [
// Connections
{
name: 'Connections',
makeNavSection: true,
makeContentSection: true,
items: (0, _sortBy.default)(groupedObjects
.map((obj) => ({
...obj,
isType: true,
}))
.filter(type => {
if (connectionArray.some(r => type.name.split(/(?=[A-Z])/).includes(r))) {
return true
}
}),
'name'),
},
// Enums
{
name: 'Enums',
makeNavSection: true,
makeContentSection: true,
items: (0, _sortBy.default)(enumTypes
.map((en) => ({
...en,
isType: true,
}))
,
'name'),
},
// Input Objects
{
name: 'Input Objects',
makeNavSection: true,
makeContentSection: true,
items: (0, _sortBy.default)(inputObjects
.map((input) => ({
...input,
isType: true,
}))
,
'name'),
},
// Objects
{
name: 'Objects',
makeNavSection: true,
makeContentSection: true,
items: (0, _sortBy.default)(groupedObjects
.map((obj) => ({
...obj,
isType: true,
}))
.filter(type => {
if ((renderedObjectsArray.includes(type.name)) || objectsExcludeArray.some(r => type.name.split(/(?=[A-Z])/).includes(r))) {
return false
} else {
return true
}
}),
'name'),
},
// Payloads
{
name: 'Payloads',
makeNavSection: true,
makeContentSection: true,
items: (0, _sortBy.default)(groupedObjects
.map((obj) => ({
...obj,
isType: true,
}))
.filter(type => {
if (["Payload"].some(r => type.name.split(/(?=[A-Z])/).includes(r))) {
return true
}
}),
'name'),
},
// Scalar
{
name: 'Scalar',
makeNavSection: true,
makeContentSection: true,
items: (0, _sortBy.default)(scalarTypes
.map((scalar) => ({
...scalar,
isType: true,
}))
,
'name'),
},
// Unions
{
name: 'Union',
makeNavSection: true,
makeContentSection: true,
items: (0, _sortBy.default)(unionTypes
.map((scalar) => ({
...scalar,
isType: true,
}))
,
'name'),
}
]
},
].filter(Boolean);
};
@matoni109 good job with this,
I am trying to do something very similar, but i am struggling to understand how to hook this code into the spectaql documentation generation flow.
If i understand correctly the result of the function in provided snippet of code is used as items field on the object returned by src/spectaql/index.js#run function.
The bit that i cant figure out is: how do i intercept the flow between the creation of this and running this through the template engine. Have u wrote ur own code that is responsible to use this as an input to template engine, or have u found the way to intercept the spectaql's run function?
Maybe i am missing something and i simply need to reference this from config.yaml in some way ?
Will appreciate any help. Thanks!
UPD: i think i figured it out, u override data arranger by providing the custom theme :) thanks anyway!