1.2.9 • Published 4 years ago

mongql v1.2.9

Weekly downloads
8
License
MIT
Repository
github
Last release
4 years ago

Mongql

A package to convert your mongoose schema to graphql schema

TOC

Features

  1. Create graphql schema (typedef and resolvers) from mongoose schema
  2. Stitch already created typedef and resolvers
  3. Easily configurable (any of the typedef and resolvers can be turned off)
  4. View the generated SDL
  5. Auto addition of graphql validators with mongoose

Motivation

  1. Creating a graphql SDL is not a difficult task by any means, but things get really cumbersome after a while, especially since a lot of the typedefs and resolvers are being repeated.
  2. Automating the schema generation helps to avoid errors regarding forgetting to define something in the schema thats been added to the resolver or vice versa.
  3. Creating resolvers for subtypes in a PITA, especially if all of them just refers to the same named key in parent

Usage

Basic Usage (Without initial typedef and resolvers)

// User.schema.js
const mongoose = require('mongoose');

const UserSchema = mongoose.Schema({
    name: {
        type: String,
        mongql: {
            writable: false // field level config
        }
    }
});

UserSchema.mongql = {
    resource: 'user'
}; // schema level config

module.exports = UserSchema;
// index.js
const {
    makeExecutableSchema
} = require('@graphql-tools/schema');
const {
    ApolloServer
} = require('apollo-server-express');
const Mongql = require('MonGql');

const UserSchema = require('./User.schema.js');

(async function() {
    const mongql = new Mongql({
        Schemas: [UserSchema],
    });

    // Calling the generate method generates the typedefs and resolvers
    const {
        TransformedResolvers,
        TransformedTypedefs, // Contains both arr and obj representation
    } = await mongql.generate();

    const GRAPHQL_SERVER = new ApolloServer({
        schema: makeExecutableSchema({
            typeDefs: TransformedTypedefs.arr,
            resolvers: TransformedResolvers.arr,
        }),
        context: () => ({
            user: req.user
        })
    });
})();

Intermediate Usage (With initial typedef and resolvers)

// user.typedef.js
module.exports = gql `
  type BasicUserType{
    name:String!
  }
`;
// user.resolver.js
module.exports = {
    Mutation: {
        updateUserSettings: ...
    }
}
const UserAST = require('./user.typedef.js');
const UserResolver = require('./user.resolver.js');

const PreTransformedTypeDefsASTs = {
    user: UserAST // This has to match with the resource name added in the mongoose schema
}

const PreTransformedResolvers = {
    user: UserResolver
}

const mongql = new Mongql({
    Schemas: [UserSchema, SettingsSchema],
    Typedefs: {
        init: PreTransformedTypeDefsASTs
    },
    Resolvers: {
        init: PreTransformedResolvers
    }
});

Intermediate Usage (Output SDL and AST)

  const mongql = new Mongql({
      Schemas: [],
      output: {
          SDL: path.resolve(__dirname, "./SDL"),
          AST: path.resolve(__dirname, "./AST")
      }
  });

  await mongql.generate()

Intermediate Usage (Fine grain Mutation configuration)

const mongql = new Mongql({
    Schemas: [UserSchema, SettingsSchema],
    generate: {
        mutation: false, // will not generate any mutation typedef and resolver,
        mutation: {
            create: false, // Will not generate any create mutation typedef and resolver,
            update: {
                multi: false // Will not generate any update multi mutation typedef and resolver
            },
            single: false // Will not generate any single mutation typedef and resolver
        }
    }
});

Intermediate Usage (Fine grain Query configuration)

const mongql = new Mongql({
    Schemas: [UserSchema, SettingsSchema],
    generate: {
        query: false,
        query: {
            all: false
        },
        query: {
            paginated: {
                self: false
            }
        },
        query: {
            filtered: {
                others: {
                    whole: false
                }
            }
        }
    }
});

Intermediate Usage (Extra Powerful Fine grain Query configuration)

const mongql = new Mongql({
    Schemas: [UserSchema, SettingsSchema],
    generate: {
        query: {
            self: false, // remove all self related typedefs and resolvers,
            self: {
                whole: false // remove all selfwhole related typedefs and resolvers,
            },
            count: false, // remove all count related typedefs and resolvers,
        }
    }
});

Advanced Usage (generating Schema and Models)

const Mongql = require('mongql');
const {
    ApolloServer
} = require('apollo-server');

(async function() {
    const mongql = new Mongql({
        Schemas: [
            /* Your schema array here */
        ],

    });
    const server = new ApolloServer({
        schema: await mongql.generateSchema(), // there is a known issue with this use makeExecutableSchema
        context: mongql.generateModels()
    });
    await server.listen();
})();

Advanced Usage (Using local folders)

const Mongql = require('mongql');
const {
    ApolloServer
} = require('apollo-server');

(async function() {
    const mongql = new Mongql({
        Schemas: path.resolve(__dirname, './schemas'),
        output: {
            dir: __dirname + '\\SDL'
        },
        Typedefs: path.resolve(__dirname, './typedefs'),
        Resolvers: path.resolve(__dirname, './resolvers')
    });
    const server = new ApolloServer({
        schema: await mongql.generateSchema(), // there is a known issue with this use makeExecutableSchema
        context: mongql.generateModels()
    });
    await server.listen();
})();

Mongql contains 3 level of configs

  1. Constructor/global level config: passed to the ctor during Mongql instantiation
  2. Schema level config: Attached to the schema via mongql key
  3. Field level config: Attached to the field via mongql key

Precedence of same config option is global < Schema < field. That is for the same option the one with the highest precedence will be used.

Configs

& refers to the whole key declared just right above.

Global Configs

NameDescriptionTypeDefault ValueUsageAvailable in
outputoutput related configurationboolean \| Objectfalse{output: false} {output: { dir: process.cwd()}}Schema
&.(dir|SDL)SDL Output directorystringundefined{output: { dir: process.cwd()}}Schema
&. ASTAST Output directorystringundefined{output: { AST: process.cwd()}}Schema
generateControls generation of type, query and mutations typedefs and resolversObject | booleantruegenerate: trueSchema
&.queryControls generation of query typedefs and resolversObject | booleanObjectgenerate :{query: true}Schema
&.(range)Controls generation of query range typedefs and resolversObject | booleanObjectgenerate :{query:{ all: false}} Take a look at concepts to see all rangesSchema
&.(auth)Controls generation of query range auth typedefs and resolversObject | booleanObjectgenerate :{query:{ all: {self: false}}} Take a look at concepts to see all authSchema
&.(part)Controls generation of query range auth part typedefs and resolversObject | booleanObjectgenerate :{query:{ all: {self: {whole: false}}}} Take a look at concepts to see all partSchema
&.mutationControls generation of mutations typedefs and resolversObject | booleantruegenerate :{mutation: true}Schema
&.(action)Controls generation of mutations typedefs and resolvers actionObject | booleantruegenerate :{mutation: {create: {multi:true}, update: {single: false}}} Take a look at concepts to see all mutation actionSchema
&.(target)Controls generation of mutations typedefs and resolvers targetObject | booleantruegenerate :{mutation: {create: {multi:true}, update: {single: false}}} Take a look at concepts to see all mutation targetsSchema
SchemasArray of schemas generate by mongoose or path to schema folderSchema[] | String[]Schemas: [UserSchema, ...]
TypedefsTypedefs related configuration or path to typedefs folderObject | String{init: undefined}Typedefs: {init: {User: InitialUserTypedef}}
&.initInitial typedefs to be attached to resultant typedefObjectundefinedinit: {User: InitialUserTypedef}
ResolversResolvers related configuration or path to resolvers foldersObject | String{init: undefined}Resolvers: {init: {User: InitialUserResolvers}}
&.initInitial resolvers to be attached to resultant resolverObjectundefinedinit: {User: InitialUserResolver}
appendRTypeToEmbedTypesKeyControls whether or not to append the resource type to sub/embed/extra typesbooleantrueappendRTypeToEmbedTypesKey: trueSchema

Schema configs

NameDescriptionTypeDefault ValueUsageAvailable in
resourcename of the resourcestringRequiredresource: User
global_excludePartitionsControls which auth partition will be excluded in the generated schemasObject{base: [], extra: ['Others', 'Mixed']}global_excludePartitions: {base: [ 'Others', 'Mixed' ]}
&.(base|extra)Controls which auth partition will be excluded in the types of generated schemas[] \| boolean{base: [], extra: ['Others', 'Mixed']}global_excludePartitions: {base: [ 'Others', 'Mixed' ], extra: ['Self']}
generateInterfaceControls whether or not to generate interface from base resourcebooleantruegenerateInterface: true
skipSkip mongql all togetherbooleanfalseskip: true

Field configs

NameDescriptionTypeDefault ValueUsageAvailable in
writableControls whether or not this field is present in generated inputbooleantruewritable: true
scalarCustom graphql scalar to be used (atm all graphql-scalars scalars are included)stringparsed type from mongoosescalar: 'NonNegativeInt'

Concept

During the generation of schema, a few concepts are followed

  1. Each Resource query object type contains four parts

    1. Range(Input):

      1. All: Gets all the resource
      2. Paginated : Used to get resource through pagination inpu
      3. Filtered : Used to get resource through filter input
      4. ID: Used to get a resource by id
    2. Auth:

      1. Self: Used to indicate logged in users resource
      2. Others: Used to indicate other users resource (when current user is authenticated)
      3. Mixed: Used to incicate others users resource (when user is unauthenticated)
    3. Resource: Name of the resource (capitalized & pluralized form)

    4. Part(Output):

      1. Whole: Get the whole data along with sub/extra types
      2. NameAndId: get the name and id of the resource
      3. Count: get the count of resource
  2. Each resource mutation object type contains 2 parts

    1. Action: One of create|update|delete
    2. Target: resource for targeting single resource, resources for targeting multiple resources
  3. Each resource types contains the following parts

    1. Based on the permitted auths types will be generated with the following syntax auth resource type and type, eg SelfUserType
    2. All the embedded mongoose schema will be converted into its own type
    3. mongoose Enums fields will be converted into enums
    4. Based on the generateInterface option interface will be generated
    5. Inputs will be created based on the generated type

Generated Query Examples: getSelfSettingsWhole, getOthersSettingsNameAndId

Generated Mutation Examples: createSetting, updateSettings

NOTE: Count part is not generate in paginated and id range as for paginated query the user already knows the count and id returns just one

API

These methods are available in the created Mongql instance

NameDescriptionParamsReturned
generate()Generate the Typedefs and resolvers from the schemas{TransformedTypedefs, TransformedResolvers}
getResources()Gets all the resources collected from all schemas(Schema.mongql.resource)[]
generateModels()Generates models from the schema providedObject:{[Schema.mongql.resource]: MongooseModel}
generateSchema()Generates a schema by calling makeExecutableSchema internallyoptions passed to makeExecutableSchema expect for typedefs and resolversGraphQLSchema
static outputSDL()Outputs the SDL from the given typedef path: String // SDL output dir typedefs: GraphqlAST | String // String or AST to convert resource: String // name of file or resource

FAQ

  1. Why do I need to use resource key?

Answer.

  1. Resource key is used to merge the initial query, mutation and types in the correct place

  2. Its used as the Model name, in the generated resolvers

  3. Its used to derive relation between resources, (not yet supported), for eg in the mutation resolver, dependant resources can be created and deleted

TODO

  1. Add more well rounded tests
  2. Migrate the whole project to TS
  3. Using babel to transpile to transform modern featues
  4. Standard liniting configuration
  5. Provide ES modules to make the library tree-shakable
  6. More enriched API
  7. Better documentation

PRS are more than welcome and highly appreciated!!!!