![Efesto](https://efesto.mabiloft.com/efesto_logo.png)

<p style="text-align:center;">A fast, easy and powerful framework for API costruction with <a href='https://nodejs.org/'>node</a> and <a href="https://www.npmjs.com/package/express">express</a></p>

---

## Main Features:

### Efesto provides a large amount of facilitations like:

- Easy and clean way to write API documentation throughout swagger
- Easy way to validate requests (body, headers etc...)
- Enable caching throughout [redis](https://redis.io)
- Easy way to authenticate your endpoints
- Support for [ABAC](https://en.wikipedia.org/wiki/Attribute-based_access_control)
- Easy error handling
- Enhance your endpoints structure only with the file/folder positioning
- Full TypeScript support

## Main Dependencies:

- [@casl/ability](https://www.npmjs.com/package/@casl/ability) for [ABAC](https://en.wikipedia.org/wiki/Attribute-based_access_control) support
- [multer](https://www.npmjs.com/package/multer) for multipart support

## Installation and Setup:

Install the efesto package into your project using:

```bash
npm i efesto@tridente-neptuni
```

in your app file use efesto as a middleware:

```typescript
  ...
  import efesto from "efesto"

  const app = express();

  app.use("/api/v1", efesto(efestoConfiguration))
```

efesto configuration object is structured like this:

|    Parameter    | Required |                                Description                                |   Default   |
| :-------------: | :------: | :-----------------------------------------------------------------------: | :---------: |
| authMiddleware  |    ✅    |  the authentication middleware - [read more](#authentication-middleware)  |      /      |
| errorMiddleware |    ✅    |   the middleware which handles errors - [read more ]("#error-handling")   |      /      |
|  isProduction   |    ✅    | Enables the production mode: the watch will be on pointes js (dist) files |      /      |
|     options     |    ❌    |                    Efesto's [options object](#options)                    | `undefined` |

### Options

|             Parameter              |                        Description                        |         Default          |
| :--------------------------------: | :-------------------------------------------------------: | :----------------------: |
|         absoluteDirRoutes          |      Path where Efesto will find the files to parse       |      `"/v1/routes"`      |
| relativeDirSwaggerDeclarationsPath | Folder where Efesto will create the swagger `.yaml` files | `"swagger-declarations"` |
|               config               |    The Efesto's [configuration](#config-object) object    |       `undefined`        |

### Config Object

|           Parameter           |                           type                            |                                                          Description                                                          | Default       |
| :---------------------------: | :-------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------: | ------------- |
|        createdAtField         |                          string                           |                                         Define the field used as createdAt timestamp                                          | `"createdAt"` |
|        updatedAtField         |                          string                           |                                         Define the field used as updatedAt timestamp                                          | `"updatedAt"` |
|            multer             | [MulterConfigurationObject](#multer-configuration-object) |                                The [multer configuration object](#multer-configuration-object)                                | `undefined`   |
|     dynamicParameterType      |                          string                           |                                               The path dynamic parameters type                                                | `"string"`    |
| defaultRequiredValueForModels |                          boolean                          |                                     `true` if all the parameters are required by default                                      | `false`       |
|        abacPermissions        |   [AbacConfigurationObject](#abac-configuration-object)   |                                    [ABAC configuration object](#abac-configuration-object)                                    | `undefined`   |
|             redis             |  [redisConfigurationObject](#redis-configuration-object)  |                                   [redis configuration object](#redis-configuration-object)                                   | `undefined`   |
|       optionalFieldType       |                          string                           | `"undefined"` or `"null"`, the type optional field should have (needed if using Efesto with [Prisma](https://www.prisma.io/)) | `"undefined"` |

---

## Configuration

The efesto configuration is **pretty easy:**

You just need to create a `baseIndex.json` file with the base configuration for swagger (servers, authentication, ecc...) in this file you can also specify general schemas

> :thought_balloon: In order to generate a Swagger UI we suggest you use [@apidevtools/swagger-parser](https://www.npmjs.com/package/@apidevtools/swagger-parser) bundling all the declarations in an unique `.json` file and, using [swagger-ui-express](https://www.npmjs.com/package/swagger-ui-express), generate the swagger UI

## Authentication Middleware

The authentication middleware it's just a simple middleware that must be given to Efesto in order to authenticate all the requests, if you do not want to authenticate the requests declare a function like this:

```typescript
(req: Request, res: Response, next: NextFunction) => {
  next();
};
```

> In your authentication middleware you can do whatever you want, remember that whatever you put inside the request can be retrieved in all the efesto middlewares

## Error Middleware

The error middleware it's a middleware where you can handle all the errors and exceptions that must be given to Efesto.

If you don't have any error to handle you can simply pass a function like this:

```typescript
(req: Request, res: Response, next: NextFunction) => {
  next();
};
```

---

## Usage

> :information_source: The following explanation assumes that you set `"swagger-declarations"` as [`relativeDirSwaggerDeclarationsPath`](#options), `"/v1/routes"` as [`absoluteDirRoutes`](#options) and declared, on express, `"/api/v1"` as main path for Efesto

### Declaring endpoints

In order to declare an Efesto's endpoint you just need to create a file inside the `"/v1/routes"` directory so, creating an `index.ts` or `index.js` (depending on your [`isProduction`](#installation-and-setup) configuration value) in `/v1/routes/users` will automatically generate the path `/api/v1/users` on your server.

In the same way creating an `anyname.ts` file in the same folder will result in the generation of the path `/api/v1/users/anyname`

### Declaring methods

this is an Efesto's file example:

```typescript
class ModelName extends BaseApiService {
  constructor() {
    super(__filename);
  }

  swaggerModel: SwaggerModel = {
    modelName: "ModelReference",
    schemas: [
      {
        name: "User",
        properties: {
          id: "number",
          name: "string",
          surname: "string",
        },
      },
    ],
  };

  _getSwagger: SwaggerOptions = {
    operationId: "getUsers",
    cache: {
      key: "`users`",
      expiresInSeconds: 600,
    },
    responses: {
      200: {
        content: {
          "application/json": {
            schema: {
              type: "array",
              items: "@User",
            },
          },
        },
      },
    },
  };

  async _get(req: express.Request, res: express.Response, next: express.NextFunction) {
    return res.sendStatus(200);
  }
}
export default ModelName;
```

Now let's analyze the example above in code chunks in order to understand the structure of the file:

First of all the file <u>**must**</u> export by default a class which extends `BaseApiService` (the class name does not matter) having as `super()` constructor function the file name (you can access it throughout the NodeJs variable `__filename`)

```typescript
class ModelName extends BaseApiService {

  constructor(){
    super(__filename);
  }

  ...

}
export default ModelName
```

The `swaggerModel` property inside the class is used to specify the _"category"_ (for example if the endpoint contains methods to control users the best _"category"_ may be `"users"` or `"user"`) of the endpoint which have to be specified in the `modelName` attribute of `swaggerModel`. Specify the modelName is highly recommended as the absence of this attribute will result in grouping all the endpoints under the `n-a` category.

In the `swaggerModel` property can also be specified swagger models, the peculiarity of those models (as all the other Efesto's models) is that, unlike in swagger, models are shared in all the Efesto project. All the models must be specified in the `schemas` attribute of `swaggerModel`

```typescript
class ModelName extends BaseApiService {

  ...

  swaggerModel: SwaggerModel = {
    modelName: "ModelReference",
    schemas: [
      {
        name: "User",
        properties: {
          id: {
            type: "number"
          },
          name: {
            type: "string"
          },
          surname: {
            type: "string"
          },
        },
      },
    ],
  };

  ...

}
export default ModelName
```

Every method (`POST`, `PUT`, `DELETE`, etc...) must have both a swagger declaration (`_<method>Swagger`) and a corresponding function (can be asynchronous or synchronous) (`_<method>(req, res, next)`) this function will be the middleware that will be triggered when a request will be done at the endpoint using, as method, `<method>`.

```typescript
class ModelName extends BaseApiService {

  ...

    _getSwagger: SwaggerOptions = { // swagger declaration for GET method
    operationId: "getUsers",
    responses: {
      200: {
        content: {
          "application/json": {
            schema: {
              type: "array",
              items: "@User",
            },
          },
        },
      },
    },
  };

  async _get(req: express.Request, res: express.Response, next: express.NextFunction) { // middleware for GET method
    return res.sendStatus(200);
  }

  ...

}
export default ModelName
```

As you can see in the above snippet, using the `@` notation (`@<schemaName>`) will result in the automatic binding and parsing between the declared schema reference and the actual declaration path in the `.yaml` file.

This will be the declaration inside the `.yaml` file:

```yaml
$ref: index.yaml#/components/schemas/User
```

#### Dynamic paths

Efesto supports dynamic endpoints throughout parameters in paths:

for example, having this file structure:

```bash
v1/
├─ routes/
   ├─ users/
      ├─ [userId].ts
```

will result in a path like this: `/api/v1/users/[userId]` where `userId` can be a `string` or `number` depending on the [`dynamicParameterType`](#config-object) value. This means that, in the `[userId]` path `request.params` will have `userId`

eg:

if someone contacts your server using this route: `api/v1/users/620d1be516cd481f1131169d`

```typescript
class ModelName extends BaseApiService {
  async _get(req: express.Request, res: express.Response, next: express.NextFunction) {
    console.log(req.params.userId); // 620d1be516cd481f1131169d
    return res.sendStatus(200);
  }
}
export default ModelName;
```

the value of `req.params.userId` will be `620d1be516cd481f1131169d`

### Request Validation

Efesto uses [express-validator](https://www.npmjs.com/package/express-validator) for validate your requests:

you can declare the validation inside the Efesto's files:

```typescript
class ModelName extends BaseApiService {

  ...

   _putValidation = [check("name").isString()]

  ...

}
export default ModelName
```

in this example the validation will search in the request if `name` is present and it's a `string`, for further information (also about the error handling) read the [express-validator](https://www.npmjs.com/package/express-validator) documentation.

### Overriding Authentication

Due to the fact that not every endpoint requires the same authentication you can override the default authentication in every method:

```typescript
class ModelName extends BaseApiService {

  ...

   _getOverrideAuth = (req: Request, res: Response, next: NextFunction)=> {
     next()
   }

  ...

}
export default ModelName
```

in this case the `GET` endpoint authentication will be overrode by the new middleware

### Using permissions (ABAC)

If you are using ABAC in your project you can specify the general permission in the swagger specification. This will not only be written in the swagger files but will be also checked before executing requests.

Be sure to configure correctly Efesto adding inside the [config object](#config-object) the required data in order to make the permissions handling work properly

The required data is contained in the ABAC configuration object:

### ABAC configuration object

the ABAC configuration object is structured as follows:

|           property            |    type    |                           description                            |  default  |
| :---------------------------: | :--------: | :--------------------------------------------------------------: | :-------: |
|            actions            | `string[]` |                    all the available actions                     |           |
|            models             | `string[]` |                     all the available models                     |           |
| checkPermissionBeforeResolver | `boolean`  | if `true` check permissions before executing the endpoint method |  `true`   |
|        reqAbilityField        |  `string`  | the `request` field where efesto can find the permission object  | `ability` |

Once you have defined all the properties you will be able to use ABAC in Efesto.

### ABAC usage

> :information_source: The following instruction are written assuming that the `reqAbilityField` parameter is set to `ability`. Furthermore in order to make the ABAC work correctly the use of the [@casl/ability](https://www.npmjs.com/package/@casl/ability) library is highly recommended

First of all, in your authentication middleware, (or in any other middleware) create the abilities

This is how you can declare required permission:

```typescript
class ModelName extends BaseApiService {

  ...

    _getSwagger: SwaggerOptions = {
    operationId: "getUsers",
    permission: ["readAll", "Users"],
    responses: {
      200: {
        content: {
          "application/json": {
            schema: {
              type: "array",
              items: "@User",
            },
          },
        },
      },
    },
  };

  ...

}
export default ModelName
```

in this case, before executing the method Efesto will check if the client has the required permission. If not a [ForbiddenError](https://casl.js.org/v5/en/api/casl-ability#forbidden-error) will be raised.

### Using Cache

Efesto has an embedded caching system based on [Redis](https://redis.io/).
In order to enable this feature you need to configure Redis when instancing Efesto.

```typescript
this.app.use(
      "/api/v1",
      efesto({
        ...
        options: {
          ...
          config: {
            redis: { // You need to declare this object in the configuration
              host: "127.0.0.1" // Redis IP/endpoint,
              port: 9091 // Redis port,
              defaultExpiresInSeconds: 600 // Redis data default Time To Live,
            },
          },
        },
      })
    );
```

Once you declared this data Efesto's caching system is ready to go!

In order to cache an endpoint response you just need to declare the corresponding Redis key:

```typescript
_getSwagger: SwaggerOptions = {
    ...
    cache: {
      key: "`users`",
    },
    ...
  };
```

Remember that the key attribute must contain a string that has to be evaluated so using backticks in it (` "``" `) is required.

Furthermore the key has access to the `req` object (the request) so you can create a dynamic key based on the request data.

Assuming that you have inside your request an `user` object composed like this:

```typescript
{
  id: "622b8386f96d33fa0c24428b",
  name: "Foo",
  surname: "Bar"
}
```

In order to have a cache based on the user in the request you will have to describe the key like this:

```typescript
_getSwagger: SwaggerOptions = {
    ...
    cache: {
      key: "`user-${req.user.id}-operationid`",
    },
    ...
  };
```

that will result (in this specific case) in the following key: `user-622b8386f96d33fa0c24428b-operationid`.

If the cached data for the endpoint is found in redis Efesto will skip the method returning the cached data.
you can also specify the specific TTL for the endpoint cache:

```typescript
_getSwagger: SwaggerOptions = {
    ...
    cache: {
      key: "`user-${req.user.id}-operationid`",
      expiresInSeconds: 60 // this cached data will last 1 minute
    },
    ...
  };
```

In order to purge the cache you have to specify which endpoint invalidates the cached data using the `purge` attribute:

```typescript
_postSwagger: SwaggerOptions = {
    ...
    purge: ["`users`"]
    ...
  };
```

the purge attribute accepts an array of Redis keys that will be deleted on the endpoint call.

Both `key` and `purge` parameters are optional and support all [Redis operators](https://redis.io/commands/KEYS)

### Uploading files

In case you need to upload files throughout your APIs Efesto uses [multer](https://www.npmjs.com/package/multer) to handle file uploads.

In order to upload files you have to declare the content of the request body as `"multipart/form-data"` and enable the multer flag:

```typescript
class ModelName extends BaseApiService {
  ...
  _postSwagger: SwaggerOptions = {
    ...
    requestBody:{
      content:{
        "multipart/form-data":{
          schema:{
            type: "object",
            properties:{
              file: {
                type: "string",
                format: "binary"
              }
            }
          }
        }
      }
    }
  };

  _postMulter: boolean = true;

  ...
}
```

using this configuration the file will be accessible in the request:

```typescript
async _post(req: Request, res: Response, next: NextFunction){

  const file = req.file // this will be the uploaded file
  ...
}
```

Efesto accepts by default single file uploads, in order to enable the ability of upload multiple files at once you need to set the property `MultipleMulter` in the endpoint declaration:

```typescript
class ModelName extends BaseApiService {
  ...
  _postSwagger: SwaggerOptions = {
    ...
    requestBody:{
      content:{
        "multipart/form-data":{
          schema:{
            type: "object",
            properties:{
              files: {
                type: "array",
                items:{
                  type: "string",
                  format: "binary"
                }
              }
            }
          }
        }
      }
    }
  };

  _postMulter: boolean = true;
  _postMultipleMulter: boolean = true // required for multiple file uploads

  ...
}
```

using this configuration the files will be accessible in the request:

```typescript
async _post(req: Request, res: Response, next: NextFunction){

  const files = req.files // this will be the array of uploaded files
  ...
}
```

### Shorthands

Writing documentation in Swagger can sometimes be redundant and confusing. For this reason Efesto has a bunch of shorthands that can lighten up the documentation writing:

- #### Schemas reference and sharing | `@Schema`

  As told before Efesto, unlike swagger, shares all the declared schemas in the whole project and can be accessible using the `@` followed by the schema name.

  > :warning: This is also the suggested way to refer to a schema due to the fact that, otherwise, you will need to specify in the `$ref` property the `.yaml` generated file path using the Swagger syntax

  Example:

  ```typescript
  class ModelName extends BaseApiService {
    constructor() {
      super(__filename);
    }

    swaggerModel: SwaggerModel = {
      modelName: "ModelReference",
      schemas: [
        {
          name: "User",
          properties: {
            id: {
              type:"number"
            },
            name: {
              type:"string"
            },
            surname: {
              type:"string"
            },
          },
        },
      ],
    };

    _postSwagger: SwaggerOptions = {
      ...
      responses:{
        200: {
          content:{
            "application/json":{
              schema:"@User"
            }
          }
        }
      }
    }
  }
  ```

- #### Formats | `type::format`

  In order to declare a format you can use the following syntax:

  ```typescript
    ...
    properties: {
      date: {
        type: "string::date-time"
      },
    },
    ...
  ```

- #### Examples | `type|example`

  In case you need to declare an example you can easily declare with this syntax:

  ```typescript
    ...
    properties: {
      name: {
        type: "string|Joe"
      },
    },
    ...
  ```

  > :information_source: This last two shorthands (format and type) are chainable together

- #### Quick Type Declaration | `propertyName: "type"`

  In the documentation each property must have a declared type and it's rare that this type will have a format or an example associated with it.

  OpenAPI documentation forces you to declare a `type` property for each attribute you add to the documentation making it pretty redundant.
  That's why Efesto introduces also a shorthand to quickly declare a property type so:

  ```typescript
    ...
    properties: {
      name: {
        type: "string"
      },
    },
    ...
  ```

  can be declared as:

  ```typescript
    ...
    properties: {
      name: "string"
    },
    ...
  ```

- #### Quick Content Declaration | `requestBody: "type|@Schema"` / `<statusCode>: "type|@Schema"`

  Each endpoint has a also a corresponding response and, sometimes also a request body; declaring them as required in the OpenAPI specification can affect your code cleanliness.
  Assuming that you are writing a login endpoint using the standard OpenAPI specification format you should end up with a snippet like this:

  ```typescript
  class ModelName extends BaseApiService {
    constructor() {
      super(__filename);
    }

    swaggerModel: SwaggerModel = {
      modelName: "ModelReference",
      schemas: [
        {
          name: "User",
          properties: {
            id: {
              type:"number"
            },
            name: {
              type:"string"
            },
            surname: {
              type:"string"
            },
          },
        },
        {
          name: "UserLoginRequestBody",
          properties:{
            username: {
              type: "string"
            },
            password:{
              type: "string"
            }
          }
        }
      ],
    };

  _postSwagger: SwaggerOptions = {
    operationId: "login",
    requestBody:{
      content:{
        "application/json":{
          schema: "@UserLoginRequestBody"
        }
      }
    }
    responses: {
      200: {
        content: {
          "application/json": {
            schema: "@User",
          },
        },
      },
    },
  };

  }
  ```

  just applying the above documented shorthands the code can be cleaned a little bit like this:

  ```typescript
  class ModelName extends BaseApiService {
    constructor() {
      super(__filename);
    }

    swaggerModel: SwaggerModel = {
      modelName: "ModelReference",
      schemas: [
        {
          name: "User",
          properties: {
            id: "number",
            name: "string",
            surname: "string",
          },
        },
        {
          name: "UserLoginRequestBody",
          properties:{
            username: "string",
            password:"string"
          }
        }
      ],
    };

    _postSwagger: SwaggerOptions = {
      operationId: "login",
      requestBody:{
        content:{
          "application/json":{
            schema: "@UserLoginRequestBody"
          }
        }
      }
      responses: {
        200: {
          content: {
            "application/json": {
              schema: "@User",
            },
          },
        },
      },
    };

  }
  ```

  but as you can see the request body and the response are extremely long and could be written in a more efficient way:

  ```typescript
  class ModelName extends BaseApiService {
    constructor() {
      super(__filename);
    }

    swaggerModel: SwaggerModel = {
      modelName: "ModelReference",
      schemas: [
        {
          name: "User",
          properties: {
            id: "number",
            name: "string",
            surname: "string",
          },
        },
        {
          name: "UserLoginRequestBody",
          properties:{
            username: "string",
            password:"string"
          }
        }
      ],
    };

    _postSwagger: SwaggerOptions = {
      operationId: "login",
      requestBody: "@UserLoginRequestBody"
      responses: {
        200: "@User"
      },
    };

  }
  ```

  in this way the verbose response declaration can be skipped and the response will be by default `"application/json"` and will have as response content (in this specific case) an object of type `User`. This will clean up your code and will help you to make it more readable and understandable.

  > :information_source: Note that this shorthand can also be used inside the schema declaration of a parameter:
  >
  > ```typescript
  >  _postSwagger: SwaggerOptions = {
  >    operationId: "login",
  >    parameters: [
  >     {in: "query", name: "exampleParam", schema: "string" }
  >    ]
  >    requestBody: "@UserLoginRequestBody"
  >    responses: {
  >      200: "@User"
  >    },
  >  };
  > ```
  >
  > instead of:
  >
  > ```typescript
  >  _postSwagger: SwaggerOptions = {
  >    operationId: "login",
  >    parameters: [
  >     {in: "query", name: "exampleParam", schema: {type: "string"} }
  >    ]
  >    requestBody: "@UserLoginRequestBody"
  >    responses: {
  >      200: "@User"
  >    },
  >  };
  > ```

With this information you should now be ready to go using Efesto!

Have a nice forging! :heart:
