Models
Bananasplit-js provides a class that you can extend for the model creation. The resulting model is more orderly than the define function provided by Sequelize.
In this class all the columns types are declared, the model definition (attributes) is a static member and also the model options. The attributes
attribute value is what usually is passed as first parameter to the sequelize define function, and $options
the second one.
To start copy tmpl.ts
-> product.ts
(in singular form).
Let's create a basic product model:
models/product.ts
/**
*
* Model: Product
* @module app/models/product
* @description model for product
*
*/
import { Model } from '@bananasplit-js'
import { DataTypes, ModelAttributes, ModelOptions } from 'sequelize'
class Product extends Model {
/**
* @columns
*/
declare uuid: number
declare name: string
declare code: string
declare price: number
/**
* @attributes
*/
static attributes: ModelAttributes = {
uuid: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
code: {
type: DataTypes.STRING,
allowNull: false
},
price: {
type: DataTypes.INTEGER.UNSIGNED
}
}
/**
* @options
*/
static $options: ModelOptions = {
timestamps: true
}
}
// Init the model
Product.init()
export default Product
When the init
method is called, the model loads the attributes and options in background.
It's important to mention the fact that the Model
class provided by Bananasplit-js extends from the Sequelize Model as well, but this logic is transparent to the developer.
Executing tasks before the export
Sometimes you may need to execute tasks before the model is exported. For this purpose you can declare a self-invoking function which in some cases can be asynchronous.
Let's synchronize the product model with the table in the database:
class Product extends Model {
// ...rest
}
;(async () => {
Product.init()
await Product.sync({ force: true })
})()
export default Product
Inside this self-invoking function you can run any task you want. For this particular case, we init the model first, then we synchronize the model with the table in the database.
Controllers
Bananasplit-js controllers are just classes with static members. Using classes allows us to have heritance and static attributes for store volatile values at memory while the server instance is up.
To start copy tmpl.ts
-> products.ts
(in plural form).
Let's code a basic products controller example:
controllers/products.ts
/**
*
* Controller: Products
*
* @module controllers/products
* @description controller for products
*
*/
import { Request, Response } from 'express'
import { IProduct } from '@interfaces/product'
/**
* @model
*/
import Product from "@models/product"
export default class ProductsController {
/**
* @description gets all the products
*/
static async index (req: Request, res: Response): Promise<Response<IProduct[]>> {
const products: IProduct[] = await Product.findAll()
return res.json(products)
}
/**
* @description gets an existent product
*/
static async show (req: Request, res: Response) {
// ...code
return res.json({ ... })
}
/**
* @description creates a new product
*/
static async create (req: Request, res: Response) {
// ...code
return res.json({ ... })
}
/**
* @description updates an existent product
*/
static async update (req: Request, res: Response) {
// ...code
return res.json({ ... })
}
/**
* @description deletes an existent product
*/
static delete (req: Request, res: Response) {
// ...code
return res.json({ ... })
}
}
This is the classic resource example. It contains 5 methods:
index
which list all the existent products (implemented)
show
which list only one particular product
create
which creates a new product
update
which updates an existent product
delete
which deletes an existent product
This controller uses the Product
model in order to retrieve the data from the database and every handler is a pure express handler.
Store data at memory
One of the advantages of using classes are attributes. Attributes members can be used in order to store data while the server is up. They will be available even between multiple requests.
Is important to mention that this sort of data is completely volatile and it will be lost once the server is restarted or down. Also should not be used if the application is running multiple instances through load balancers. On those cases is better to use a database instead.
Let's check an example that registers the client ip at every request:
// ...rest
export default class ProductsController {
// clients ip collection
private static ips = new Set<string>()
static async index (req: Request, res: Response): Promise<Response<IProduct[]>> {
// registry the client ip
const ip: string = req.headers['x-forwarded-for'] ?? req.socket.remoteAddress
this.ips.add(ip)
// retrieve all the products
const products: IProduct[] = await Product.findAll()
return res.json(products)
}
}
The index
method will run at every request but the ips
attribute value will persist over the time. Notice that ips
has a private scope. This restrict the access to inside the controller only.
Accessing the express settings
In some cases you may want to access the express settings inside controllers. This is very useful to read or edit them at runtime.
Let's wrap the index method inside a high order function:
import { Application, Request, Response, RequestHandler } from 'express'
// ...rest
export default class ProductsController {
static index (app: Application): RequestHandler {
return async (req: Request, res: Response): Promise<Response<IProduct[]>> => {
// access the setting
const setting: string = app.get('key')
// do something with it
// retrieve all the products
const products: IProduct[] = await Product.findAll()
return res.json(products)
}
}
}
Now the index
method receives the app
express instance as parameter and returns the request handler as arrow function.
To finish, add the following change into the route definition:
routes/products.routes.ts
// ...rest
export default (app: Application): Router => {
$.route('/products')
.get(ProductsController.index(app))
}
Calling the function in this way we keep the this
reference pointing to the controller itself.
Routes
Routes works exactly as in express.
To start copy tmpl.routes.ts
-> products.routes.ts
(in plural form).
The .routes
suffix is very important because it tells to bananasplit-js to not ignore this file.
Let's code a basic route example to listing products:
routes/products.routes.ts
/**
*
* Router: Products
*
* @module app/routes/products
* @description routes for products
*
*/
import { Router } from 'express'
/**
* @controller
*/
import ProductsController from "@controllers/products"
const $: Router = Router()
export default (): Router => {
$.route('/products')
.get(ProductsController.index)
return $
}
What this file does is very simple:
- It imports the product controller from
@controllers
- It creates a
new router
, so you have entire control over it
- It creates a new route
/products
assigning the index
controller method
- It exports a
function
by default which returns the created router
For convenience the router is named $
, this is simple and clean to chain multiple controller functions to the same route:
export default (): Router => {
$.route('/products')
.get(ProductsController.index)
.post(ProductsController.create)
return $
}
This exported function let us to access the express instance and settings inside the routes files, which are shared to the entire application:
import { Application, Router } from "express"
// ...rest
export default (app: Application): Router => {
const setting: any = app.get('key')
// do something with it
$.route('/products')
.get(ProductsController.index)
return $
}
This must be used to read only and not to set.
Automatic routes detection
Once a routes file is created, Bananasplit-js will detect it automatically adding it to the express application.
All routers are incorporated to the express application by default, this behavior is declared in the middlewares/express.ts
file.
Read more about: Managing how routers are added to the express application.
All files without .routes
suffix will be ignored, this is an excellent way to deactivate routers while developing.
This routes definitions are pure express code. Check the express documentation: Express Routing.
Default routes
If your project doesn't have route files with valid routes defined in it Banansplit-js will serve a default /
route returning the following response from server:
Otherwise, this default route will be automatically ignored and replaced with yours.
Middlewares
Middlewares are just express request handler functions which receives an extra third param: next
.
All middlewares functions are exported from middleware files which are located in the middlewares
folder. Those files describes a group of middlewares and exports so many request handler functions as you need.
Let's create a simple auth middleware for the products route:
middlewares/auth.ts
/**
*
* Middleware: Auth
*
* @module middlewares/auth
* @description authentication middlewares
*
*/
import { Request, Response, NextFunction } from 'express'
import passport from 'passport'
/**
* Basic Auth Middleware
* @description checks for authentication before get the routes
*/
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const authenticate = await passport.authenticate('jwt', { session: false })
// passport will call next() automatically if the token is valid
authenticate(req, res, next)
}
Then you can import the auth middleware and add it to any route - method you want:
routes/products.routes.ts
import { authMiddleware } from "@middlewares/auth"
// ...rest
export default (): Router => {
$.route('/products')
.get([authMiddleware], ProductsController.index)
.post([authMiddleware], ProductsController.create)
return $
}
Also, is possible to add the middleware to the entire router:
import { authMiddleware } from "@middlewares/auth"
// ...rest
export default (): Router => {
/**
* @middlewares
*/
$.use([authMiddleware])
$.route('/products')
.get(ProductsController.index)
.post(ProductsController.create)
return $
}
The usage you see in the code above is pure express middlewaring and routing. The possibilities are multiple but is recommended to use it in this way.
To check if the middlewares were applied correctly, use the route:list
built-in command.
Please check: Listing routes and middlewares in the command-line.
The "Express" Middleware
Bananasplit-js includes a main middleware file which is plugged-in to Express.
This file is very important because contains a variety of pre-built middlewares and the route modules subscriptions for the entire application.
It can be found at middlewares/express.ts
middlewares/express.ts
import Express, { Application, Router } from 'express'
import { IRouters } from '@bananasplit-js/interfaces'
// ...rest
export default (app: Application, routers: IRouters): void => {
// ...rest
// use all routers by default
Object.values(routers).forEach((router: Router) => app.use(router))
}
This file exports a function by default which receives two parameters:
app
the Express application instance
routers
a key-value based object where the key is the name of each route module contained at the app/routes
directory, and value the express router object itself exported from that module.
The following line is the most important:
Object.values(routers).forEach((router: Router) => app.use(router))
Because it tells to Express to use all the routers exported from each file in the app/routes
directory.
If you want to have more control in which router is used, then you can manage them manually.
For example, a routes directory like this:
├── products.routes.ts
├── categories.routes.ts
└── users.routes.ts
Routers object would look like:
{
products: [Router],
categories: [Router],
users: [Router]
}
[Router]
is a Router object instance provided by Express.
So, you can add them one by one:
app.use(routers.products)
app.use(routers.categories)
This main middleware provides to you full control over the built-in middlewares and routers activation.
Helpers
Nothing simpler than export a function which helps bigger functions to do minor tasks.
Helpers are just isolated portions of code that helps high level functions to become lighter.
You can export too many functions as you want and organizate them by files. Each filename must describe a group of functions.
Let's create a helper function which maps the product code into a category:
helpers/products.ts
/**
*
* Helpers: Products
* @description helper functions for products
*
*/
/**
* Map Product Code to Category
* @description takes the first 5 characters of the code and maps them into a category
*/
export const mapProductCodeToCat = (code: string): string => {
const categories: { [prefixCode: string]: string } = {
'fjw32': 'keyboards',
'3n2io': 'screens',
'mdnf2': 'racks',
'oi4o3': 'earphones',
default: 'no category'
}
return categories[code.substr(0, 5)] ?? categories.default
}
This function can be used from anywhere, for example, in the products controller:
controllers/products.ts
// ...rest
import { mapProductCodeToCat } from '@helpers/products'
export default class ProductsController {
static async index (req: Request, res: Response): Promise<Response<Array<IProduct & { category: string }>>> {
const products: IProduct[] = await Product.findAll()
// add category for each product
const productsWithCategory: Array<IProduct & { category: string }> = products.map((p: IProduct) => {
// map product code into a category
const category: string = mapProductCodeToCat(p.code)
return { ...p, category }
})
return res.json(productsWithCategory)
}
}
This controller will return exactly the same products
array than the older one but with an extra property: category
.
Migrations
Migrations are like a version control of your tables, you can create the table, update the fields, modify the constraints, etc, all backed up in a registry that sequelize manages.
The Sequelize CLI can help on this task.
Let's create a migration for the products table:
npx sequelize migration:generate --name create-products-table
This will generate a new migration file in the database/migrations
directory.
Notice that the first part of the file name starts with a timestamp, that is the exact moment when the migration was created, and most important, the fullname describes the action that the migration is performing.
Let's continue writing the migration:
Optionally, you can copy the content from tmpl.ts
-> 20201229085157-create-products-table.ts
migrations/20201229085157-create-products-table.js
/**
*
* Migration: Products
* @description migration for the products table
*
*/
export function up (queryInterface, DataTypes) {
return queryInterface.createTable('Products', {
uuid: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
code: {
type: DataTypes.STRING,
allowNull: false
},
price: {
type: DataTypes.INTEGER.UNSIGNED
},
createdAt: {
type: 'TIMESTAMP',
defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
allowNull: false
},
updatedAt: {
type: 'TIMESTAMP',
defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
allowNull: false
}
})
}
export function down (queryInterface, Sequelize) {
return queryInterface.dropTable('Products')
}
This migrations mainly performs the creation of the products table including their timestamps.
All migration files contains two member functions:
up
which is in charge to create the table
down
which does exactly the reverse action: drop the table.
To create the table run the following command:
to drop:
npx sequelize db:migrate:undo
Generators
Bananasplit-js generators are just functions that are able to generate a row of data that can be saved in the database tables when executing seeders. Also they can be used to generate testing data for your tests.
All generators are located at database/generators
and they use the Faker library to generate random data.
They must be named following the convention: create-{resource}
. For example: create-product
.
Since Sequelize CLI doesn't provides Typescript support, generators must be written in Javascript just like seeders.
Let's create a generator for the products table:
To start copy tmpl.js
-> create-product.js
(in singular form).
database/generators/create-product.js
/**
*
* Generator: Product
* @description creates a product object based on fake data
*
*/
const Generator = require('@bananasplit-js/utils/generator')
const faker = require('faker')
// date for timestamps
const date = new Date()
// generator
const createProduct = (product = {}) => ({
// columns
uuid: faker.datatype.uuid(),
name: faker.commerce.product(),
code: faker.internet.password().toLowerCase(),
price: parseInt(faker.commerce.price()),
// timestamps
createdAt: date,
updatedAt: date,
// extending
...product
})
module.exports = Generator(createProduct)
This file contains a common function which receives only one parameter: product
(the resource), with an empty object as default value.
This value is used in order to extend the generated product or partially pass a product
to merge it with the generated one.
Generators files must export a generator function wrapped by the high order function Generator
which is provided by Bananasplit-js. This Generator
function provides extra functionalities to the original one.
This product generator function can be used in any file:
const createProduct = require('@generators/create-product')
// completely random
const product1 = createProduct()
// passing values
const product2 = createProduct({
name: 'Macbook M1 Pro',
price: 1999
})
Output example:
// product1
{
uuid: '97154f7b-2502-4677-9c7b-3ac42e44471d',
name: 'Keyboard',
code: 'Ii1RbaWe6gNf1Yl',
price: 626,
createdAt: 2022-08-07T01:38:37.096Z,
updatedAt: 2022-08-07T01:38:37.096Z
}
// product2
{
uuid: '6a12f49f-d5ad-4145-b549-e9580aef6c07',
name: 'Macbook M1 Pro',
code: '1ZmYCW_L0ygGwiT',
price: 1999,
createdAt: 2022-08-07T01:38:37.096Z,
updatedAt: 2022-08-07T01:38:37.096Z
}
Generating amounts of data
Thanks to the extra functionalities provided by the Generator
high order function, is possible to generate amounts of data:
// completely random array
const products1 = createProduct.amount(10)
// passing values to each array element
const products2 = createProduct.amount(10, {
price: 899
})
The lines above will create an array
of 10 products with completely random data and a second one of the same length but with a static price
value for all of them.
Seeders
Seeders are basically loaders that populate your tables with fake data, this is very useful to develop and test your application.
To create a seeder file the best option to use is the Sequelize CLI.
Sequelize CLI doesn't provide native Typescript support, therefore, seeders must be written in Javascript.
Let's create a seeder for the products table:
npx sequelize seeder:generate --name products-seeder
Optionally, you can copy the content from tmpl
-> 20201229085902-products-seeder.js
seeders/20201229085902-products-seeder.js
/**
*
* Seeder: Products
* @description seeds the products table
*
*/
const createProduct = require("@generators/create-product")
exports.up = (queryInterface) => {
const products = createProduct.amount(10)
return queryInterface.bulkInsert('Products', products, {})
}
exports.down = (queryInterface) => {
return queryInterface.bulkDelete('Products', null, {})
}
Like migrations, seeder files contains the same two member functions:
up
which is in charge to add registries to the table
down
which does exactly the reverse action: truncate the table.
This seeder file uses a generator function which allow us to generate amount of registries of the same type.
Please check: Creating Generators
Tests
Bananasplit-js includes Jest and supertest integrated ready to use.
Test files are located in the /tests
directory at the root of the project.
To start writting a test you may need to import first the express
and Sequelize
instances:
import { express } from '@services'
import { Sequelize } from '@bananasplit-js'
Using Express + supertest
Once the express
instance is imported, you need to run the server:
// ...rest
// Express server
let Express: http.Server
beforeAll(() => {
Express = express.serve(6627)
})
We execute a function before run all the tests, which start listening the express server in the port 6627
. The server instance returned is stored in a global variable Express
in order to be accessed by the entire file.
Once the Express server is running, we can write a simple test using supertest:
// ...rest
import request, { Response } from 'supertest'
/**
* @test Express server is ok
*/
test('Express server is ok', async () => {
const response: Response = await request(Express).get('/')
expect(response.status).toBe(200)
expect(response.text).toBe('GET 200 /')
})
Note
Tests will run in paralel at the port 6627 not colliding with your development server.
Using Sequelize
In order to query data from the database using Sequelize, you can just use the Sequelize
instance directly:
Closing connections
After run the tests is very important to close the connections used by Express and Sequelize in order to avoid issues:
afterAll(async () => {
// close express
Express.close()
// close sequelize
await Sequelize.close()
})
We execute a function after all the tests are finished. In the first instruction the Express connection is closed, in the second one the Sequelize connection.
Warning
Not closing connections correctly can cause issues during the tests.
Running the tests
You can run all the tests by running:
Or run specific tests by running:
yarn test products | npm test products
It's possible to pass one or more specific tests.
Built-in scripts
Bananasplit-js includes a set of scripts already defined you can just use:
dev |
start the development server using nodemon and registering path aliases |
build |
build the application to /dist running static files copying and executing cleaning actions |
build:database |
create, migrate and seed the database in one command |
build:stack |
install and test the entire application stack in just one command |
upgrade:stack |
upgrade all the dependencies running the tests (doctor mode) |
route:list |
list all routes and middlewares defined in the application |
generator:create |
create resources with fake data using generators in the command-line |
test |
run tests with jest |
test:watch |
run tests with jest in watch mode |
test:coverage |
run tests with jest generating coverage documentation |
test:cache |
clear jest cache |
lint |
check lint in your code |
lint:fix |
lint and fix your code |
Listing routes in command-line
Bananasplit-js features router-dex module which allows to you to list your routes and middlewares in the command-line.
The route:list
command will give you additional information, for example, if you have controller functions binded or if you are using anonymous functions. It also will group all your routes by basepath and order them by alphabet.
Run in the command-line:
yarn route:list | npm run route:list
It will output something like this:
method |
route |
middlewares |
|
GENERAL |
|
GET |
/ |
login |
|
PRODUCTS |
|
GET |
/product/:id |
auth, show |
POST |
/product/:id |
auth, create |
|
USERS |
|
GET |
/users |
auth, *isAdmin, index |
|
USER |
|
DELETE |
/user/:id |
auth, *isAdmin, delete |
|
|
|
This is very useful especially when you have a lot of routes and you need to visualize them in a more orderly way.
Filtering by groups
All routes belongs to a group which is determined by the route basepath. This group name is displayed above each route group.
For example, in the output above: general, products, user and users are groups.
Let's filter by product routes only:
Also is possible to filter by multiple groups, for example, product and user:
yarn route:list product user
You can pass many filters as you want.
Symbols and meanings
In the middleware column of the generated table you can see the following notations:
*
means the middleware/controller function passed is binded controller.bind(ref)
anonymous
means the middleware/controller function was passed directly into the router/app
Generating data in command-line
Sometimes you will need to test your application from REST clients like Postman, Insomnia or PAW.
For this reason Bananasplit-js allow to you to generate data from the command-line by running the built-in script generator:create
followed of the resource name, which is the name of the generator file omitting the "create-" part.
For example, for the create-product.js
generator you should use product
as resource name.
Run in the command-line:
yarn generator:create product | npm run generator:create product
You will see the resource as output in the console. The data will be automatically copied to the clipboard, ready to be paste in any REST client or text editor.
Generating resources in command-line is a very good way to check if your generator is working as expected.
To generate amounts of data just pass a second parameter with the quantity:
yarn generator:create product 10
To extend just pass a third parameter '--extend' containing a valid json object as string:
yarn generator:create product 10 --extend='{ "price": 899 }'
If the json object is too large, you can help yourself by using shell variables:
product='{ "name": "Macbook M1 Pro", ..., "price": 899 }'
yarn generator:create product 10 --extend=$product
Adapters
Frecuently is needed to change the data column names when this is being served or sent to the API. For example in products, one table column can be named price
but served in the API or received as JSON body under the name of cost
or total
.
For this reason Bananasplit-js generators includes the ability to adapt the data when is being generated from command-line.
Let's modify the products generator to add an adapter:
database/generators/create-product.js
/**
*
* Generator: Product
* @description creates a product object based on fake data
*
*/
const Generator = require('@bananasplit-js/utils/generator')
const faker = require('faker')
// date for timestamps
const date = new Date()
// generator
const createProduct = (product = {}) => ({
// ...rest
})
// the adapter
const adapter = {
'uuid': 'id',
'price': 'total'
}
// adapt the data
module.exports = Generator(createProduct, adapter)
What this modification does is, first, define a new adapter
object where the key is the current column name of the table and value the new key name.
Finally, this adapter
object is passed as second parameter to the Generator
high order function.
Is important to be aware that by default adapters are only activated when the data is being generated in command-line and not internally in our files.
Command-line output example with adapted data:
{
id: '97154f7b-2502-4677-9c7b-3ac42e44471d',
name: 'Keyboard',
code: 'Ii1RbaWe6gNf1Yl',
total: 626,
createdAt: 2022-08-07T01:38:37.096Z,
updatedAt: 2022-08-07T01:38:37.096Z
}
Notice that now price
-> total
and uuid
-> id
.
In order to activate the adapter programatically in the generator, pass an extra adapt
boolean parameter to the createProduct
(create resource) function:
const createProduct = require('@generators/create-product')
// completely random with adapted data
const product1 = createProduct({}, true)
// passing values with adapted data
const product2 = createProduct({
name: 'Macbook M1 Pro',
total: 1999
}, true)
This true
parameter will force the generator to return adapted data.
Upgrading dependencies
Bananasplit-js includes a tool to easily upgrade all the package dependencies in secure mode (doctor mode). It uses the npm-check-update library in background.
How it works?
The doctor mode iteratively installs upgrades and runs tests to identify breaking upgrades.
To upgrade run in command-line:
yarn upgrade:stack | npm run upgrade:stack
Ignore list
In some cases you may need to ignore some dependencies because breaking changes or incompatibility issues.
In order to do that modify the .ncurc.json
file located at the project root:
.ncurc.json
{
"upgrade": true,
"reject": [
// ignored dependencies list
]
}
The reject
array contains a list of package names to be ignored, no matter what.
Express
Express comes already configurated and no options object is needed. Anyway you may need to change the default port.
Port
The port where Express runs by default is 3327
but you can specify a different one in the providers/services.ts
file.
Let's make Express to listen on port 3000:
providers/services.ts
// ...rest
// Express server provider
const express: Express = Express.provide({ port: 3000 })
Now Express will start listeting on port 3000
.
Also is possible to specify the port in the environment variables:
.env
# ...rest
# [Express]
PORT=3000
This is very useful for deployment scenarios.
Sequelize
The Sequelize configuration object (connection pool) is located at config/sequelize/sequelize.conf.js
and extends the default provided by the Sequelize Provider in the makeOptions
method.
config/sequelize/sequelize.conf.ts
// ...rest
const Options: Sequelize.Options = {
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
},
sync: {}
}
This file is exposed in order to customize the Sequelize configuration as you want.
Sequelize CLI
Similar to above, the Sequelize CLI configuration is located at config/sequelize/sequelize-cli.conf.js
and extends the defaults provided by the main configuration file.
config/sequelize/sequelize-cli.conf.ts
module.exports = {
/**
* development
*/
development: {
dialectOptions: {
bigNumberStrings: true
}
},
/**
* test
*/
test: {
dialectOptions: {
bigNumberStrings: true
}
},
/**
* production
*/
production: {
dialectOptions: {
bigNumberStrings: true,
ssl: {}
}
}
}
This file is exposed in order to customize the Sequelize CLI configuration as you want.
Using SQLite with Sequelize
SQLite requires an additional parameter:
You will need to add the following property to the config/sequelize/sequelize.conf.ts
file:
config/sequelize/sequelize.conf.ts
// ...rest
const Options: Sequelize.Options = {
// storage path
storage: 'path/to/database.sqlite',
// ...rest
}
Done.
This directory contains all the settings files related to the application.
Bananasplit-js only includes defaults settings for express, but you can add your owns.
Express
Express provides the methods get
and set
in order to manage global settings. This settings are shared into the entire application.
In order to add or remove settings just edit the settings/express.ts
file.
Let's set a simple setting:
settings/express.ts
// ...rest
export default (app: Application) => {
// ...rest
// my setting
app.set('key', 'value')
}
This setting value can be accessible from anywhere where you have access to the app
Express Application instance:
// app scope
(app: Application) => {
// my setting value
const setting: string = app.get('key')
}
For example inside routes or even controllers.
Please check: Accessing the express settings inside controllers.
Public folder
Bananasplit-js already have set the public directory used by Express.
You can change it by modifying the following line:
app.set('public', path.join(__dirname, '../../public'))
Bananasplit-js
This template provides tools to easy copy any sort of file to the /dist
folder at build time.
The settings for this functionality are stored in the bananasplit.json
file located at the root of the project.
By default the file looks like this:
bananasplit.json
{
"dist": {
"include": [
"public"
],
"exclude": [
],
"options": {
"overwrite": true
}
}
}
All the copy settings are declared inside the dist
object:
-
include:
File or folder pathname collection used to copy to the dist
folder.
Value can be a string
or [string, string]
where the first element is the source and the second one the destiny (optional).
-
exclude:
File or folder pathname collection to be ignored from the include
list, no matter what.
Value is only a string
array.
-
options:
- overwrite:
boolean
value that allows bananasplit-js to overwrite the file or folder if already exists.
Let's add files and directories to copy:
{
"dist": {
"include": [
"public",
".github/workflows",
["docs/about/project.md", "docs/project.md"]
],
"exclude": [
".github/workflows/dev.yml"
],
"options": {
"overwrite": true
}
}
}
In this case, Bananasplit-js will copy:
public
-> dist/public
.github/workflows
-> dist/.github/workflows
docs/about/project.md
-> dist/docs/project.md
(new destiny)
And it will ignore:
.github/workflows/dev.yml
(copying all the rest)
The provider settings
By default Bananasplit-js includes their own settings in background that are in charge to copy essentials files to the dist
folder.
Example: LICENSE
, tsconfig.paths.json
, jest.config.js
, etc.
You can find this file at providers/core/bananasplit.json
in case you want to make some modifications.
The core
Core is the most important part of Bananasplit-js. It can be a little unnoticed because the folder is sort of hidden, but exist.
This part of the template is in charge of store service providers, jobs, default routes and controllers, auto-loaders, core helpers, default configurations, etc.
"As inside is outside"
The core folder structure is similar to the structure you see inside the src
directory.
providers/core
└── app/
│ ├──── bootstrap.ts
│ ├──── controllers/
│ └──── routes/
├── config/
├── helpers/
├── interfaces/
├── jobs/
├── libs/
├── utils/
├── bananasplit.json
└── index.ts
Let's check what each part is in charge to:
/app
contains default routes, controllers and the services bootstrap.
/config
stores default configurations, for example, sequelize.
/helpers
stores isolated parts of logic in order to make provider classes lighter.
/interfaces
stores shared types and interfaces alogn the core.
/jobs
stores the built-in commands scripts.
/libs
stores the service providers classes.
/utils
stores utilities and resources which are exposed to the developer.
bananasplit.json
default bananasplit-js settings file.
index.ts
bananasplit-js core entry point.
Even though Bananasplit-js is built in order to avoid touch the core, by changing any of those files you will be able to change the complete Bananasplit-js behavior.
Only is recommended to do if you really need to cover very specifics and advanced needs.
Extending Providers
Bananasplit-js is built on their own Service Providers. Providers are classes that provides services to the template.
By default Bananasplit-js dispose of two:
Providers are located in the providers/core/libs
directory and you can modify them as you want, even create your own ones.
The Express Provider
To start let's understand how the provider works. This is a simplified class representation:
providers/core/libs/express.ts
class ExpressProvider {
public port = 3627
private service: Express.Application
private static instance: ExpressProvider
private constructor () { /* implements singleton */ }
public static provide (config?: IC): ExpressProvider { /* ...magic */ }
public static getInstance (): ExpressProvider { /* ...magic */ }
public serve (port?: number): http.Server { /* ...magic */ }
public getApplication (): Express.Application { /* ...magic */ }
private settings (config?: IC): void { /* ...magic */ }
private middlewares (routers: IRouters): void { /* ...magic */ }
private routers (): IRouters { /* ...magic */ }
}
Attributes
-
public
-
private
-
private static
Constructor
(Not used because the class implements singleton pattern).
Methods
The ExpressProvider
class uses the providers/core/helpers/resources.ts
helper file in order to simplify the logic.
This helper file includes a couple of functions that allows Bananasplit-js to read different kind of modules and load them.
Once you are able to understand how the ExpressProvider
class is implemented you can start changing their behavior or adding more logic to it.
The Sequelize Provider
This provider is quite similar to the Express Provider. Part of it is based in the same class structure but it includes his own methods.
providers/core/libs/sequelize.ts
class SequelizeProvider {
private service: Sequelize
private static instance: SequelizeProvider
private constructor () { /* implements singleton */ }
public static provide (): SequelizeProvider { /* ...magic */ }
public static getInstance (): SequelizeProvider { /* ...magic */ }
public getApplication (): Sequelize { /* ...magic */ }
private makeAuth (): DBAuth | string { /* ...magic */ }
private makeOptions (): Options { /* ...magic */ }
}
Attributes
Constructor
(Not used because the class implements singleton pattern).
Methods
The Sequelize Provider, contrary to the Express Provider, doesn't use helpers. All his logic is defined in this simple class.
Conclusion
By modifying the providers methods and attributes you will be able to extend their functionallity as you want.
This is recommended for very specific needs only.
Addons
Nothing like freedom.
The addons directory is located at providers/addons
and his purpose is basically just to provide a folder where to store new customized providers.
If you need to add more service providers to your application, then this is the right place.
Recommendations
It's recommended to write all your service providers as class based entities, following software design patterns if neccessary.