npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@edonec/route-generator

v4.2.2

Published

Route Generator for eDonec Turborepo Boilerplate

Downloads

109

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 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"
      }
    }
  ]
}

| 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 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.