0.0.1 • Published 7 years ago

decorator-x v0.0.1

Weekly downloads
4
License
ISC
Repository
github
Last release
7 years ago

npm.io npm version

中文版本 | English Version

Decorate Once, Use Everywhere - Decorator For JavaScript and Node.js Application

swagger-decorator

Original intention of swagger-decorator is to simplify JavaScript Application(Web & Node.js)development,by reusing information defined in the entity (plain JavaScript class). Entity decorated by swagger-decorator can be used in instance creation and validation, Sequelize ORM Model generation, autogenerated Swagger doc for Koa2, etc. However, things will develop in opposite direction when they become extreme, so if you feel exhausted with too much decorator, just stop using it.

# use npm to install 

$ npm install swagger-decorator -S

$ npm install babel-plugin-transform-decorators-legacy -D

# use yarn to install

$ yarn add swagger-decorator

$ yarn add babel-plugin-transform-decorators-legacy -D

# import function or decorator you need
import { 
    wrappingKoaRouter,
    entityProperty,
    ...
} from "swagger-decorator";

entity decorator

/**
 * Description description for this property
 * @param type 
 * @param description 
 * @param required 
 * @param defaultValue 
 * @param pattern
 * @param primaryKey 
 * @returns {Function}
 */
export function entityProperty({
  // these params will be used in swagger doc generation
  type = "string",
  description = "",
  required = false,
  defaultValue = undefined,

  // this param is used in validation
  pattern = undefined,

  // this param is used in orm model
  primaryKey = false
}) {}

We can use entityProperty to define simple User entity class:

// @flow

import { entityProperty } from "../../src/entity/decorator";
import UserProperty from "./UserProperty";
/**
 * Description User Entity
 */
export default class User {

  @entityProperty({
    type: "integer",
    description: "user id, auto-generated",
    required: true
  })
  id: string = 0;

  @entityProperty({
    type: "string",
    description: "user name, 3~12 characters",
    required: false
  })
  name: string = "name";

  @entityProperty({
    type: "string",
    description: "user email",
    pattern: "email",
    required: false
  })
  email: string = "email";

  @entityProperty({
    type: UserProperty,
    description: "user property",
    required: false
  })
  property: UserProperty = new UserProperty();
}

export default class UserProperty {
  @entityProperty({
    type: ["number"],
    description: "user friends, which is user ids",
    required: false
  })
  friends: [number];
}

Swagger Inner Datatype:

Common NametypeformatComments
integerintegerint32signed 32 bits
longintegerint64signed 64 bits
floatnumberfloat
doublenumberdouble
stringstring
bytestringbytebase64 encoded characters
binarystringbinaryany sequence of octets
booleanboolean
datestringdateAs defined by full-date - RFC3339
dateTimestringdate-timeAs defined by date-time - RFC3339
passwordstringpasswordUsed to hint UIs the input needs to be obscured.

instance generation and validation

After defining entity class, we can use instantiate to generate instance for this entity class. Different from the new keyword, instantiate will assign property with entity instance other than plain object recrusively; and it will also validate data.

/**
 * Description 
 * @param EntityClass  
 * @param data  
 * @param ignore  
 * @param strict  
 * @throws  
 */
export function instantiate(
  EntityClass: Function,
  data: {
    [string]: any
  },
  { ignore = false, strict = true }: { ignore: boolean, strict: boolean } = {}
): Object {}

Here we use jest:

describe("test instantiate", () => {
  test("test validation", () => {
    expect(() => {
      instantiate(User, {
        name: "name"
      }).toThrowError(/validate fail!/);
    });

    let user = instantiate(User, {
      id: 0,
      name: "name",
      email: "a@q.com"
    });

    expect(user).toBeInstanceOf(User);
  });

  test("test ignore param, which is used to ignore validation", () => {
    instantiate(
      User,
      {
        name: "name"
      },
      {
        ignore: true
      }
    );
  });

  test("test strict param, if set true will ignore external property which isn't defined in entity class", () => {
    let user = instantiate(
      User,
      {
        name: "name",
        external: "external"
      },
      {
        ignore: true,
        strict: true
      }
    );

    expect(user).not.toHaveProperty("external", "external");

    user = instantiate(
      User,
      {
        name: "name",
        external: "external"
      },
      {
        ignore: true,
        strict: false
      }
    );

    expect(user).toHaveProperty("external", "external");
  });
});

describe("test nested entity property", () => {
  test("test User", () => {
    let user = instantiate(User, {
      id: 0,
      property: {
        friends: [0]
      }
    });

    expect(user.property).toBeInstanceOf(UserProperty);
  });
});

Sequelize Model

const originUserSequelizeModel = generateSequelizeModel(
  User,
  {
    _id: {
      primaryKey: true
    }
  },
  {
    mappingCamelCaseToUnderScore: true
  }
);

const UserSequelizeModel = sequelize.define(
  "b_user",
  originUserSequelizeModel,
  {
    timestamps: false,
    underscored: true,
    freezeTableName: true
  }
);

UserSequelizeModel.findAll({
  attributes: { exclude: [] }
}).then(users => {
  console.log(users);
});

generate entity class from flow type

Here we use babel and baylon to extract type information from flow file:

// @flow

import { flowToDecorator } from '../../../../src/transform/entity/flow/flow';

test('测试从 Flow 中提取出数据类型并且转化为 Swagger 接口类', () => {
  flowToDecorator('./TestEntity.js', './TestEntity.transformed.js').then(
    codeStr => {
      console.log(codeStr);
    },
    err => {
      console.error(err);
    }
  );
});

Soucrce Entity:

// @flow

import AnotherEntity from "./AnotherEntity";

class Entity {
  // Comment
  stringProperty: string = 0;

  classProperty: Entity = null;

  rawProperty;

  @entityProperty({
    type: "string",
    description: "this is property description",
    required: true
  })
  decoratedProperty;
}

Transformed Entity:

// @flow

import { entityProperty } from 'swagger-decorator';

import AnotherEntity from './AnotherEntity';

class Entity {
  // Comment
  @entityProperty({
    type: 'string',
    required: false,
    description: 'Comment'
  })
  stringProperty: string = 0;

  @entityProperty({
    type: Entity,
    required: false
  })
  classProperty: Entity = null;

  @entityProperty({
    type: 'string',
    required: false
  })
  rawProperty;

  @entityProperty({
    type: 'string',
    description: 'this is property description',
    required: true
  })
  decoratedProperty;
}

API Decorator and Swagger Doc Generation

Wrapping router

import { wrappingKoaRouter } from "swagger-decorator";

...

const Router = require("koa-router");

const router = new Router();

wrappingKoaRouter(router, "localhost:8080", "/api", {
  title: "Node Server Boilerplate",
  version: "0.0.1",
  description: "Koa2, koa-router,Webpack"
});

// define default route
router.get("/", async function(ctx, next) {
  ctx.body = { msg: "Node Server Boilerplate" };
});

// use scan to auto add method in class
router.scan(UserController);

Defining API with decorator

export default class UserController extends UserControllerDoc {
  @apiRequestMapping("get", "/users")
  @apiDescription("get all users list")
  static async getUsers(ctx, next): [User] {
    ctx.body = [new User()];
  }

  @apiRequestMapping("get", "/user/:id")
  @apiDescription("get user object by id, only access self or friends")
  static async getUserByID(ctx, next): User {
    ctx.body = new User();
  }

  @apiRequestMapping("post", "/user")
  @apiDescription("create new user")
  static async postUser(): number {
    ctx.body = {
      statusCode: 200
    };
  }
}

too much decorator in UserController may decrease the readability of code, so we can move some description to its parent class:

export default class UserControllerDoc {
  @apiResponse(200, "get users successfully", [User])
  static async getUsers(ctx, next): [User] {}

  @pathParameter({
    name: "id",
    description: "user id",
    type: "integer",
    defaultValue: 1
  })
  @queryParameter({
    name: "tags",
    description: "user tags, for filtering users",
    required: false,
    type: "array",
    items: ["string"]
  })
  @apiResponse(200, "get user successfully", User)
  static async getUserByID(ctx, next): User {}

  @bodyParameter({
    name: "user",
    description: "the new user object, must include user name",
    required: true,
    schema: User
  })
  @apiResponse(200, "create new user successfully", {
    statusCode: 200
  })
  static async postUser(): number {}
}

run your application

  • run your application and open swagger docs (PS. swagger-decorator contains Swagger UI):
/swagger

npm.io

/swagger/api.json

npm.io