amur

    0.8.6 • Public • Published

    Amur

    NPM version Build Status Test Coverage Dependency Status License PR Welcome

    Amur is the ultimate GraphQL scaffolding tool.

    It automates coding process to save your precious time, enhance your work and life experience. In other words, amur write code for you with commands you specified.

    With amur, you can create a full-fledged backend server with your complex app logic and running API in less than 3 minutes.

    Documentation

    Motivation

    When working on GraphQL projects, we need to define database schema and GraphQL twice. We need to create resolvers for the standardized API. A lot of copying and pasting are going on. It's not elegant to copy code around and find-replace all occurrences. It's also error prone. And sometimes causing unnoticeable errors which wastes time.

    Wouldn't be nice if we could have a tool just like Ruby on Rails' scaffold tool to generate code for us?

    Design Concept

    Amur is designed to provide features at least Ruby on Rails scaffold tool has. Aka, generate boilerplate business logic code as much as possible for you.

    However, unlike Ruby on Rails, amur is not a full-fledged framework and will never provide a framework for you. It focus on business logic generation. Although amur also has project generation feature, it's trying to hook up the industry standard and battle-tested libraries and components together for you. And it's configurable. To split features into small core chunks and make them combinable and adaptable is a good practice and especially popular in node.js ecosystem, amur embraces this practice. That's what makes amur outstanding and what makes amur really a flexible and configurable scaffolding tool.

    Installation

    Amur is a general command line tool, thus you should install it globally.

    npm install -g amur

    Create an Amur Project

    To create an amur project, use amur app command.

    amur app my-new-app

    This will generate your app in 'my-new-app' folder. If you don't specify app name, the app will be created at your current working directory.

    Options:

    • --port On which port this app is listening on.
    • --git-init Automatically run 'git init' after project generated.
    • --skip-install Do not install dependencies.
    • --eslint-config Changing the default eslint config.
    • --main Changing the entry filename.

    To change default eslint config being used:

    amur app my-new-app --eslint-config=your-config

    To automatically run git init:

    amur app my-new-app --git-init

    Generate Resources

    API resource generation is the core feature of amur. It's syntax is rather simple and extensible. It follows this basic style:

    amur resource ModelName[/optionalPluralVariableName] \
    primitiveField[[:Type[typeModifiers]]:defaultValue]... \
    referenceField[[:ReferenceType[typeModifiers]]:foreignKey]...

    This arguments specification is obscure to see. Let's see some examples.

    Let's say you have a model named user, and user has a name, an age and also a list of posts. And you have a model named post, it has title, content and author. Just type like this:

    amur resource User name:String age:Int posts:[Post]:author
    amur resource Post title:String content:String author:User

    Here we specified our first model 'User', with following fields:

    • name which is a String
    • age which is an Int
    • posts which is a list of Posts through foreign key named author

    We defined our second model named 'Post', with following fields:

    • title which is a String
    • content which is also a String
    • author which references to User

    This creates six files in total, three for User and three for Post. The three files are mongoose model, GraphQL schema and GraphQL resolver.

    The autogenerated models/User.js looks like this:

    const mongoose = require('mongoose');
    const { Schema } = mongoose;
     
    const userSchema = new Schema({
      name: String,
      age: Number
    }, {
      timestamps: true,
      collection: 'users'
    });
     
    module.exports = mongoose.model('User', userSchema);

    The autogenerated schemas/User.gql looks like this:

    type User {
      _id: ID!
      name: String
      age: Int
      posts: [Post]
      createdAt: Date
      updatedAt: Date
    }
     
    input UserInput {
      name: String
      age: Int
    }
     
    type Query {
      user(_id: ID!): User
      users: [User]
    }
     
    type Mutation {
      createUser(input: UserInput): User
      updateUser(_id: ID!, input: UserInput): User
      deleteUser(_id: ID!): User
    }

    The autogenerated resolvers/User.js looks like this:

    module.exports = {
      User: {
        async posts(root, _, { Post }) {
          return await Post.find({ author: root._id });
        }
      },
      Query: {
        async user(root, { _id }, { User }) {
          return await User.findById(_id);
        },
        async users(root, { _ }, { User }) {
          return await User.find();
        }
      },
      Mutation: {
        async createUser(root, { input }, { User }) {
          return await User.create(input);
        },
        async updateUser(root, { _id, input }, { User }) {
          return await (await User.findById(_id)).set(input).save();
        },
        async deleteUser(root, { _id }, { User }) {
          return await (await User.findById(_id)).remove();
        }
      }
    };

    Besides your schema definition, 5 API are created for you. Those are:

    • users query all users
    • user query a user by id
    • createUser create a new user
    • updateUser modify an existing user
    • deleteUser delete an existing user

    Now you can CRUD your resources through API.

    Primitive Types

    Amur supports a wide range of primitive types:

    • String string type
    • Int integer type
    • Float float type
    • Boolean bool type
    • Date date type
    • Enum enum type, the type specifier has a different syntax
    • File upload typem the type specifier has a different syntax
    • Mixed mixed type includes string, int, float, boolean, date, array and objects

    When you are defining a field with type mentioned above, amur will treat them as primitive types. When you refer to a type that is not included in the list, amur will treat it as a referecing to another model.

    amur resource User disabled:Boolean name:String description:Mixed spouse:User

    In the above example, obviously disabled, name and description are primitive types. spouse is a reference type which references to User.

    Array Type

    Surround a type with a pair of [], you get an array of that type, for example:

    amur resource User spouse:User friends:[User] favoriteSayings:[String]
    

    The field friends is an array of Users. And the field favoriteSayings is an array of Strings.

    Reference Types

    There are several ways to implement your own reference types.

    one-to-one

    The simplest case is one-to-one relation ship.

    amur resource User address:Address
    amur resource Address user:User:address

    In this case, we save the reference into user model, and on address model, we use the foreign key on user model to fetch the user value.

    one-to-many

    We have two ways to implement this relationship.

    amur resource User posts:[Post]:owner
    amur resource Post user:User:owner

    This is the most common case. We save the reference on the 'many' side, and fetch on the 'many' side model.

    amur resource User posts:[Post]
    amur resource Post user:User:[posts]

    In this case, we are saving the references on the 'one' side, and on 'many' side, we use a pair of [] to indicate it's an array. Be careful of performance when you are doing this way.

    many-to-many

    In simple cases, we can just do like this.

    amur resource User courses:[Course]
    amur resource Course users:[User]:[courses]

    If there are tons of records, then you may want to use association table.

    amur resource Favorite user:User course:Course
    amur resource User courses:[Course]:Favorite
    amur resource Course users:[User]:Favorite

    In this case, we specified a relationship that is have many ... through ...

    Upload Type

    To create an uploading field, use ...Uploader as type name. See the following example:

    amur resource User avatar:AvatarUploader

    To create an uploader, see Generate Uploader

    Type Modifiers

    In the real world practices, fields should be validated. For example, You may want a user's email to match designated format and to be required and unique. You can specify type modifiers.

    amur resource User 'email:String/.*@.*\..*/!$'

    In the above example, /.*@.*\..*/ means that this field matches this regexp, ! means required, and $ means unique.

    Existing type modifiers includes:

    • ! required
    • ^ index
    • $ unique
    • /regexp/ string only, matches the regexp or not
    • <=n max for number types, maxlength for string type
    • >=n min for number types, minlength for string type

    Default Values

    You can specify default value to a primitive field with the following syntax.

    amur resource Post 'title:String!:Untitled' 'lastUpdate:Date!:`Date.now`'

    Here, title's default value is 'Untitled', and lastUpdate's default value is Date.now. It's a calculated default value, so surround with a pair of back ticks.

    Nested Structure

    To create nested structure, use the following syntax:

    amur resource User posts:[{ title:String content:String comments:[{ \
    commenter:User content:String }] }] email:String password:String settings:{ \
    sms:Boolean email:Boolean pushNotification:Boolean }

    Specify type as { or [{, you are going into a nested context. All field defined after this goes into the nested structure. Use plain } and }] tokens to jump out the nesting context.

    Enums

    To create enum fields, use enum syntax like this:

    amur resource User 'gender:Enum(male,female)!'

    Reusable Nestables

    Amur supports reusable nestables and referencing them.

    amur nestable Address line1:String line2:String country:String region:String
    amur resource User address:addressSchema name:String

    Specify the lowercase nestable name append by 'Schema', amur will treat the type as a subschema reference.

    Destroy Resources

    If you mistakenly generated something or you spell something wrongly, use the 'destroy' command to delete the autogenerated files. Just append destroy with the original command, it automatically destroys the generated content.

    amur destroy resource User name:String

    Generate Uploader

    To generate an uploader, use amur uploader command.

    amur uploader FileUploader extends AliOSSUploader bucket=your-bucket-name region=your-region

    This generates an base file uploader for you.

    Integrate with Existing Project

    Amur is designed to be a generic tool. It does not require a project to be a amur project.

    Customize Amur Behavior

    Create a file called .amurrc.json in project's root directory. And filling it like this:

    {
      "schemaDir": "graphql",
      "resolverDir": "graphql",
      "test": false
    }

    Amur will generate schema files and resolver files into graphql directory, and will not generate unit tests.

    Issues and Helps

    Amur is not mature yet. If you find anything uncomfortable or confuses you. Any discuss, issue and pull request are welcome.

    Roadmap

    Amur is an ambitious project and it still has a long way to go.

    • Version 0.8
      • Basic sequelize support
    • Version 0.9
      • CLI user experience
      • use eslint to transform user generated code if available
      • configurability
      • dependencies reliability
    • Version 0.10
      • query filter, sorting and pagination feature

    License

    The GNU General Public License v3.0.

    Install

    npm i amur

    DownloadsWeekly Downloads

    51

    Version

    0.8.6

    License

    GPL-3.0

    Unpacked Size

    118 kB

    Total Files

    80

    Last publish

    Collaborators

    • cheunghy