4.2.2 • Published 2 months ago

@edonec/route-generator v4.2.2

Weekly downloads
-
License
MIT
Repository
-
Last release
2 months ago

eDonec Boilerplate Route Generator

JSON based route and model generation tool for eDonec Boilerplate

Installation

yarn global add @edonec/route-generator
egen-route

Usage

Usage of this tool requires a .json file as an input for that describes either the route or the model to generate.

The following section will focus on the different ways to create the .json file.

Route Generation

Route generation (as its name may imply) focuses on generating a single route, either by integrating with an existing route, or by creating a new router. it is not responsible in the implementation details as this is left to the developer to handle.

Minimal Example

The following .json file represents the minimum amount of information that needs to be included.

{
  "baseUrl": "/say-hi",
  "name": "greeting",
  "method": "GET",
  "response": "string"
}
FieldDescription
baseUrlThe URL of the route which will be concatenated with the router's base URL
nameThe name of the function (in services, controllers, validators, sdk, etc..) and it will be concatenated with a description derived from the http method provided
methodThe HTTP method of this route. Should be one of GET, POST, PUT and DELETE. The name of the function will be derived from the method (in the given example, the final function name will be getGreeting)
responseThe type of the response as every route should provide a response to the client.

The .json file can also include other route details such as the type of the request body expected from the response. The following example shows all the available fields.

{
  "baseUrl": "update-by-group/:group",
  "name": "users",
  "method": "PUT",
  "params": {
    "group": "string"
  },
  "query": {
    "token": "string"
  },
  "body": {
    "name": "string"
  },
  "response": "string"
}
FieldDescription
paramsThe URL params of the route. Each key in this object should also be included in the baseUrl field prefixed by a :. This should not be a nested type
queryThe URL query elements of the route. This should not be a nested type
bodyThe body of the request of the route

Types

The type system is inspired by the mongoose Schema declaration syntax.

By default each type is required, so the following json input:

{
  "first": "string"
}

Will translate to the following TypeScript type:

type GeneratedType = {
  first: string;
};

Types can be nested as long as the leaves are one of the following primitives : string, number, boolean and Date.

{
  "name": {
    "firstname": "string",
    "lastname": "string"
  },
  "age": "number",
  "birthday": "Date",
  "isActive": "boolean"
}
type GeneratedType = {
  name: {
    firstname: string;
    lastname: string;
  };
  age: number;
  birthday: Date;
  isActive: boolean;
};

To represent arrays simply wrap the type in [] as follows:

{
  "friends": ["string"]
}
type GeneratedType = {
  friends: Array<string>;
};

Note: only the first element in the array is taken into account as we do not support tuples yet.

To mark types as optional, we have to use the $type and $required keywords as follows:

{
  "first": {
    "$type": "string",
    "$required": false
  }
}
type GeneratedType = {
  first?: string;
};

To represent that a field has multiple type options, we have to use the $or keyword:

{
  "age": {
    "$or": ["number", "string"]
  }
}
type GeneratedType = {
  age: number | string;
};

These are the building blocks that allows the developer to add as much complexity as they see fit by composing them as shown in this (wildly unrealistic) example :

{
  "baseUrl": "/",
  "name": "users",
  "method": "GET",
  "query": {
    "sortDirection": {
      "$type": { "$or": ["number", "string"] },
      "$required": false
    },
    "page": {
      "$type": "number",
      "$required": false
    }
  },

  "body": [
    {
      "_id": "string",
      "name": {
        "firstname": "string",
        "lastname": "string"
      },
      "birthday": {
        "$type": "Date",
        "$required": false
      },
      "age": {
        "$type": { "$or": ["number", "string"] }
      }
    }
  ],
  "response": "string"
}
type GeneratedType = {
  query: {
    sortDirection?: number | string;
    page?: number;
  };
  body: Array<{
    _id: string;
    name: {
      firstname: string;
      lastname: string;
    };
    birthday?: Date;
    age: number | string;
  }>;
  response: string;
};

Validation

As part of route generation, this tool generates a validation middleware function if the input json file includes at least one of these attributes : body, params and query. The validation is based on the FieldValidator package.

By default, we generate a basic validator depending on the primitive type as follows:

TypeValidator
stringisString
numberisNumber
DateisDate
booleanisBoolean

In order to apply extra validators, we have to resort to the $validate keyword as follows:

{
  "age": {
    "$type": "number",
    "$validate": ["isPositive"]
  }
}
export const validatorFunction = (req, res, next) => {
   ..
   validators.validate.age.isNumber().isPositive();
   ..
};

To apply validators that require extra parameters, we can use the rule and param keywords as follows:

{
  "age": {
    "$type": "number",
    "$validate": [
      "isPositive",
      { "rule": "isBetween", "param": { "min": 0, "max": 130 } }
    ]
  }
}
export const validatorFunction = (req, res, next) => {
   ..
   validators.validate.age.isNumber().isPositive().isBetween({ min: 0, max: 130 });
   ..
};

Access Control

By default, all generated routes are unprotected. So the following .json file

{
  "baseUrl": "/:id",
  "name": "user",
  "method": "PUT",
  "params": {
    "id": "string"
  },
  "response": "string"
}

generates the following route:

router.put(
  `${BASE_ROUTE}/:id`,
  userValidators.updateUser,
  userController.updateUser
);

In order to apply route protection, we can use the ACL keyword as follows:

{
  "ACL": {
    "resource": "USERS",
    "privilege": "READ"
  }
}
FieldDescription
resourceAn access resource from node/shared-types/auth/AccessResources.ts
privilegeA mininum privilege on the given resource. Should be one of READ_SELF, WRITE_SELF, DELETE_SELF, READ, WRITE, DELETE, GRANT and REVOKE

So the following json:

{
  "baseUrl": "/:id",
  "name": "user",
  "method": "PUT",
  "params": {
    "id": "string"
  },
  "response": "string",
  "ACL": {
    "resource": "USERS",
    "privilege": "READ"
  }
}

generates the following route

router.putProtected(ACCESS_RESOURCES.USERS, PRIVILEGE.READ)(
  `${BASE_ROUTE}/:id`,
  userValidators.updateUser,
  userController.updateUser
);

Model Generation

Model generation aims to generate basic CRUD routes for a given model and, in contrast with route generation, the implementation details of the service functions are generated (since in this case they are deterministic).

Under the hood, model generation works by generating a mongoose schema (and its related types), an access resource and producer events. Finally, we make 5 calls to the regular route generation with different route parameters and response types.

Similarly to the Route generation, Model Generation requires a .json file describing the model to generate.

{
  "name": "Test",
  "resource": "TEST",
  "schema": {
    "isDeleted": "boolean",
    "isBanned": {
      "$type": "boolean",
      "$default": true
    },
    "name": {
      "firstname": "string",
      "lastname": "string"
    },
    "birthday": {
      "$type": "Date",
      "$required": false
    },
    "age": "number"
  }
}
FieldDescription
nameThe name of the model (ex: User, Product, Transcation, etc ..)
resourceThe access resource to attribute to this model, used in generating access protected routes. This should preferably be the model's name in CONSTANT_CASE
schemaDescription of the model to generate using the aformentioned type system.

Note: we can use the $default keyword to specify a default value in the generated mongoose schema.

This example then generates the following mongoose schema:

const schema = new Schema<TestType, TestModel>(
  {
    isDeleted: { type: Boolean, required: true },
    isBanned: { type: Boolean, required: true, default: true },
    name: {
      firstname: { type: String, required: true },
      lastname: { type: String, required: true },
    },
    birthday: { type: Date },
    age: { type: Number, required: true },
  },
  { timestamps: true }
);

Sub Schemas

We can extract duplicate type declarations in their own sub schema by using the subSchemas keyword as follows:

{
  "subSchemas": [
    {
      "name": "Name",
      "schema": {
        "firstname": "string",
        "lastname": "string"
      }
    }
  ]
}
FieldDescription
nameName describing the sub schema
schemaType declaration of said schema

We can then reference the sub schema in the main model's schema by prepending $ to the sub schema's name

{
  "name": "Test",
  "resource": "TEST",
  "schema": {
    "isDeleted": "boolean",
    "isBanned": {
      "$type": "boolean",
      "$default": true
    },
    "name": {
      "$type": "$Name"
    },
    "birthday": {
      "$type": "Date",
      "$required": false
    },
    "age": "number",
    "friendList": ["$UserListEntry"],
    "blockList": ["$UserListEntry"]
  },
  "subSchemas": [
    {
      "name": "Name",
      "schema": {
        "firstname": "string",
        "lastname": "string"
      }
    },
    {
      "name": "UserListEntry",
      "schema": {
        "user": "string",
        "addedOn": "Date"
      }
    }
  ]
}
const Name = new Schema({
  firstname: { type: String, required: true },
  lastname: { type: String, required: true },
});
const UserListEntry = new Schema({
  user: { type: String, required: true },
  addedOn: { type: Date, required: true },
});

const schema = new Schema<TestType, TestModel>(
  {
    isDeleted: { type: Boolean, required: true },
    isBanned: { type: Boolean, required: true, default: true },
    name: { type: Name, required: true },
    birthday: { type: Date },
    age: { type: Number, required: true },
    friendList: [{ type: UserListEntry, required: true }],
    blockList: [{ type: UserListEntry, required: true }],
  },
  { timestamps: true }
);

Generating sub schemas also generates the corresponding TypeScript types in the related api-types directory :

export type Name = {
  firstname: string;
  lastname: string;
};
export type UserListEntry = {
  user: string;
  addedOn: Date;
};
export type TestType = {
  isDeleted: boolean;
  isBanned: boolean;
  name: Name;
  birthday?: Date;
  age: number;
  friendList: Array<UserListEntry>;
  blockList: Array<UserListEntry>;
};

Predefined Sub Schemas

  • $BucketFile : A representation of a file uploaded to the Bucket microservice. It includes the Bucket's _id and the file's name, key, type and url
{
  "schema": {
    "file": "$BucketFile"
  }
}

Outputs to:

const schema = new Schema<FileType, FileModel>(
 file: {
      type: {
        key: { type: String, required: true },
        type: { type: String, required: true },
        name: { type: String, required: true },
        _id: { type: String, required: true },
        url: { type: String, required: true },
      },
      required: true,
    },
)

Note: currently, a sub schema can not reference other sub schemas.

4.2.2

2 months ago

4.2.1

2 months ago

4.1.0

9 months ago

4.2.0

8 months ago

4.0.4

1 year ago

4.0.6

1 year ago

4.0.0

2 years ago

4.0.3

2 years ago

4.0.2

2 years ago

1.3.4

2 years ago

1.3.2

2 years ago

1.3.1

2 years ago

1.3.0

2 years ago

1.2.7

2 years ago

1.2.6

2 years ago

1.2.5

2 years ago

1.2.4

2 years ago

1.2.3

2 years ago

1.2.2

2 years ago

1.2.1

2 years ago

1.2.0

2 years ago

1.1.0

2 years ago