@edonec/route-generator
v4.2.2
Published
Route Generator for eDonec Turborepo Boilerplate
Downloads
57
Keywords
Readme
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"
}
| Field | Description |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| baseUrl | The URL of the route which will be concatenated with the router's base URL |
| name | The name of the function (in services, controllers, validators, sdk, etc..) and it will be concatenated with a description derived from the http method provided |
| method | The 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
) |
| response | The 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"
}
| Field | Description |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| params | The 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 |
| query | The URL query elements of the route. This should not be a nested type |
| body | The 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:
| Type | Validator | | ------- | --------- | | string | isString | | number | isNumber | | Date | isDate | | boolean | isBoolean |
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"
}
}
| Field | Description |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| resource | An access resource from node/shared-types/auth/AccessResources.ts
|
| privilege | A 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"
}
}
| Field | Description |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | The name of the model (ex: User
, Product
, Transcation
, etc ..) |
| resource | The access resource to attribute to this model, used in generating access protected routes. This should preferably be the model's name in CONSTANT_CASE
|
| schema | Description of the model to generate using the aformentioned type system. |
Note: we can use the
$default
keyword to specify a default value in the generatedmongoose
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"
}
}
]
}
| Field | Description | | ------ | ------------------------------- | | name | Name describing the sub schema | | schema | Type 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 theBucket
's_id
and the file'sname
,key
,type
andurl
{
"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.