spectaql icon indicating copy to clipboard operation
spectaql copied to clipboard

Add support for grouping Queries or Mutations

Open haris1998-web opened this issue 3 years ago • 8 comments

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.

haris1998-web avatar Jul 01 '22 17:07 haris1998-web

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.

newhouse avatar Jul 01 '22 18:07 newhouse

@haris1998-web did you have any success with that?

newhouse avatar Jul 11 '22 15:07 newhouse

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.

felixvd avatar Jul 29 '22 02:07 felixvd

Related to https://github.com/anvilco/spectaql/issues/442

newhouse avatar Jul 29 '22 14:07 newhouse

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.

newhouse avatar Jul 30 '22 19:07 newhouse

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.

goochjs avatar Nov 09 '22 10:11 goochjs

@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 ruby developer so place excuse my js skills
  • you'll need lodash / microfibre in your project.
  • genericServiceGenerator does 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 avatar Jan 31 '23 04:01 matoni109

@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!

shotmk avatar Feb 23 '23 11:02 shotmk