Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,12 @@ In the route creation you can override the same settings of the plugin registrat
- `onExceeding` : callback that will be executed each time a request is made to a route that is rate limited
- `onExceeded` : callback that will be executed when a user reached the maximum number of tries. Can be useful to blacklist clients

### Examples of Custom Store

These examples show an overview of the `store` feature and you should take inspiration from it and tweak as you need:

- [Knex-SQLite](./example/example-knex.js)
- [Sequelize-PostgreSQL](./example/example-sequelize.js)

<a name="license"></a>
## License
Expand Down
6 changes: 3 additions & 3 deletions example/example-custom.js → example/example-knex.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use strict'

// Custom Store using Knex.js, below is an example
// table to store rate limits that must be created in the
// the database first
// Example of a Custom Store using Knex.js ORM for SQLite database
// Below is an example table to store rate limits that must be created
// in the database first
//
// CREATE TABLE "RateLimits" (
// "Route" TEXT,
Expand Down
185 changes: 185 additions & 0 deletions example/example-sequelize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
'use strict'

// Example of a Custom Store using Sequelize ORM for PostgreSQL database

// Sequelize Migration for "RateLimits" table
//
// module.exports = {
// up: (queryInterface, { TEXT, INTEGER, BIGINT }) => {
// return queryInterface.createTable(
// 'RateLimits',
// {
// Route: {
// type: TEXT,
// allowNull: false
// },
// Source: {
// type: TEXT,
// allowNull: false,
// primaryKey: true
// },
// Count: {
// type: INTEGER,
// allowNull: false
// },
// TTL: {
// type: BIGINT,
// allowNull: false
// }
// },
// {
// freezeTableName: true,
// timestamps: false,
// uniqueKeys: {
// unique_tag: {
// customIndex: true,
// fields: ['Route', 'Source']
// }
// }
// }
// )
// },
// down: queryInterface => {
// return queryInterface.dropTable('RateLimits')
// }
// }

const fastify = require('fastify')()
const Sequelize = require('sequelize')

const databaseUri = 'postgres://username:password@localhost:5432/fastify-rate-limit-example'
const sequelize = new Sequelize(databaseUri)
// OR
// const sequelize = new Sequelize('database', 'username', 'password');

// Sequelize Model for "RateLimits" table
//
const RateLimits = sequelize.define(
'RateLimits',
{
Route: {
type: Sequelize.TEXT,
allowNull: false
},
Source: {
type: Sequelize.TEXT,
allowNull: false,
primaryKey: true
},
Count: {
type: Sequelize.INTEGER,
allowNull: false
},
TTL: {
type: Sequelize.BIGINT,
allowNull: false
}
},
{
freezeTableName: true,
timestamps: false,
indexes: [
{
unique: true,
fields: ['Route', 'Source']
}
]
}
)

function RateLimiterStore (options) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that moving this class to a file would let the user understand better how to organize the source.

Moreover, this would avoid having a global sequelize object and it would be really more useful to users to adapt to their fastify instance

(I saw there is the same issue in the knex example and I think it should be improved as well - not an issue with this PR)

this.options = options
this.route = ''
}

RateLimiterStore.prototype.routeKey = function routeKey (route) {
if (route) this.route = route
return route
}

RateLimiterStore.prototype.incr = async function incr (key, cb) {
const now = new Date().getTime()
const ttl = now + this.options.timeWindow
const cond = { Route: this.route, Source: key }

const RateLimit = await RateLimits.findOne({ where: cond })

if (RateLimit && parseInt(RateLimit.TTL, 10) > now) {
try {
await RateLimit.update({ Count: RateLimit.Count + 1 }, cond)
cb(null, {
current: RateLimit.Count + 1,
ttl: RateLimit.TTL
})
} catch (err) {
cb(err, {
current: 0
})
}
} else {
sequelize.query(
`INSERT INTO "RateLimits"("Route", "Source", "Count", "TTL")
VALUES('${this.route}', '${key}', 1,
${(RateLimit && RateLimit.TTL) || ttl})
ON CONFLICT("Route", "Source") DO UPDATE SET "Count"=1, "TTL"=${ttl}`
)
.then(() => {
cb(null, {
current: 1,
ttl: (RateLimit && RateLimit.TTL) || ttl
})
})
.catch(err => {
cb(err, {
current: 0
})
})
}
}

RateLimiterStore.prototype.child = function child (routeOptions = {}) {
const options = Object.assign(this.options, routeOptions)
const store = new RateLimiterStore(options)
store.routeKey(routeOptions.routeInfo.method + routeOptions.routeInfo.url)
return store
}

fastify.register(require('../../fastify-rate-limit'),
{
global: false,
max: 10,
store: RateLimiterStore,
skipOnError: false
}
)

fastify.get('/', {
config: {
rateLimit: {
max: 10,
timeWindow: '1 minute'
}
}
}, (req, reply) => {
reply.send({ hello: 'from ... root' })
})

fastify.get('/private', {
config: {
rateLimit: {
max: 3,
timeWindow: '1 minute'
}
}
}, (req, reply) => {
reply.send({ hello: 'from ... private' })
})

fastify.get('/public', (req, reply) => {
reply.send({ hello: 'from ... public' })
})

fastify.listen(3000, err => {
if (err) throw err
console.log('Server listening at http://localhost:3000')
})