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
26 changes: 26 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,29 @@ jobs:
run: npx playwright install --with-deps
- name: Run Mysql Tests
run: npm run test:${{ matrix.mysql.command }}
test-sqlite:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
services:
redis:
image: redis
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Install
run: npm install
- name: Run SQLite Tests
run: npm run test:sqlite
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"test": "npm run test:pg && npm run test:mysql && mkdir coverage/tmp && cp -r coverage/*/tmp/. coverage/tmp && c8 report",
"test:pg": "cross-env DB=pg c8 --reporter=json --report-dir=coverage/pg npm run quick:test",
"test:mysql": "cross-env DB=mysql c8 --reporter=json --report-dir=coverage/mysql npm run quick:test",
"test:sqlite": "cross-env DB=sqlite c8 --reporter=json --report-dir=coverage/sqlite npm run quick:test",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This script should be added to the Github workflows for tests to run for SQLite

"clean": "del-cli build",
"typecheck": "tsc --noEmit",
"copy:templates": "copyfiles \"stubs/**/*.stub\" --up=\"1\" build",
Expand Down Expand Up @@ -47,6 +48,7 @@
"@japa/expect-type": "^2.0.2",
"@japa/file-system": "^2.3.1",
"@japa/runner": "^3.1.4",
"@libsql/sqlite3": "^0.3.1",
"@release-it/conventional-changelog": "^9.0.3",
"@swc/core": "^1.10.1",
"@types/node": "^22.10.2",
Expand All @@ -70,7 +72,7 @@
"typescript": "^5.7.2"
},
"dependencies": {
"rate-limiter-flexible": "^5.0.4"
"rate-limiter-flexible": "^6.2.1"
},
"peerDependencies": {
"@adonisjs/core": "^6.12.1",
Expand Down
61 changes: 58 additions & 3 deletions src/stores/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import string from '@adonisjs/core/helpers/string'
import { RuntimeException } from '@adonisjs/core/exceptions'
import type { QueryClientContract } from '@adonisjs/lucid/types/database'
import { RateLimiterMySQL, RateLimiterPostgres } from 'rate-limiter-flexible'
import { RateLimiterMySQL, RateLimiterPostgres, RateLimiterSQLite } from 'rate-limiter-flexible'

import debug from '../debug.js'
import RateLimiterBridge from './bridge.js'
Expand All @@ -30,9 +30,14 @@ export default class LimiterDatabaseStore extends RateLimiterBridge {

constructor(client: QueryClientContract, config: LimiterDatabaseStoreConfig) {
const dialectName = client.dialect.name
if (dialectName !== 'mysql' && dialectName !== 'postgres') {
if (
dialectName !== 'mysql' &&
dialectName !== 'postgres' &&
dialectName !== 'better-sqlite3' &&
dialectName !== 'sqlite3'
) {
throw new RuntimeException(
`Unsupported database "${dialectName}". The limiter can only work with PostgreSQL and MySQL databases`
`Unsupported database "${dialectName}". The limiter can only work with PostgreSQL, MySQL, and SQLite databases`
)
}

Expand Down Expand Up @@ -90,6 +95,56 @@ export default class LimiterDatabaseStore extends RateLimiterBridge {
this.#client = client
this.#config = config
break
case 'better-sqlite3':
super(
new RateLimiterSQLite({
storeType: 'knex',
storeClient: client.getWriteClient(),
tableCreated: true,
dbName: config.dbName,
tableName: config.tableName,
keyPrefix: config.keyPrefix,
execEvenly: config.execEvenly,
points: config.requests,
clearExpiredByTimeout: config.clearExpiredByTimeout,
duration: string.seconds.parse(config.duration),
inMemoryBlockOnConsumed: config.inMemoryBlockOnConsumed,
blockDuration: config.blockDuration
? string.seconds.parse(config.blockDuration)
: undefined,
inMemoryBlockDuration: config.inMemoryBlockDuration
? string.seconds.parse(config.inMemoryBlockDuration)
: undefined,
})
)
this.#client = client
this.#config = config
break
case 'sqlite3':
super(
new RateLimiterSQLite({
storeType: 'knex',
storeClient: client.getWriteClient(),
tableCreated: true,
dbName: config.dbName,
tableName: config.tableName,
keyPrefix: config.keyPrefix,
execEvenly: config.execEvenly,
points: config.requests,
clearExpiredByTimeout: config.clearExpiredByTimeout,
duration: string.seconds.parse(config.duration),
inMemoryBlockOnConsumed: config.inMemoryBlockOnConsumed,
blockDuration: config.blockDuration
? string.seconds.parse(config.blockDuration)
: undefined,
inMemoryBlockDuration: config.inMemoryBlockDuration
? string.seconds.parse(config.inMemoryBlockDuration)
: undefined,
})
)
this.#client = client
this.#config = config
break
}
}

Expand Down
6 changes: 3 additions & 3 deletions tests/define_config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,10 @@ test.group('Define config', () => {
})

const limiter = new LimiterManager(await config.resolver(app))
expectTypeOf(limiter.use).parameters.toMatchTypeOf<
['redis' | 'db' | 'memory' | undefined, LimiterConsumptionOptions]
expectTypeOf(limiter.use).parameters.toEqualTypeOf<
[LimiterConsumptionOptions] | ['redis' | 'db' | 'memory', LimiterConsumptionOptions]
>()
expectTypeOf(limiter.use).returns.toMatchTypeOf<Limiter>()
expectTypeOf(limiter.use).returns.toEqualTypeOf<Limiter>()

assert.isNull(
await limiter.use('redis', { duration: '1 min', requests: 5 }).get('ip_localhost')
Expand Down
8 changes: 7 additions & 1 deletion tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ export function createDatabase() {
sqlite: {
client: 'better-sqlite3',
connection: {
filename: join(test.context.fs.basePath, 'db.sqlite3'),
filename: ':memory:',
},
},
libsql: {
client: 'libsql',
connection: {
filename: join(test.context.fs.basePath, `file:libsql.db`),
},
},
pg: {
Expand Down
11 changes: 7 additions & 4 deletions tests/stores/database.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ import { createDatabase, createTables } from '../helpers.js'
import LimiterDatabaseStore from '../../src/stores/database.js'

test.group('Limiter database store | wrapper', () => {
test('throw error when trying to use connection other than mysql or pg', async () => {
test('throw error when trying to use connection other than mysql, sqlite or pg', async () => {
const db = createDatabase()
await createTables(db)

new LimiterDatabaseStore(db.connection('sqlite'), {
new LimiterDatabaseStore(db.connection('libsql'), {
dbName: 'limiter',
tableName: 'rate_limits',
duration: '1 minute',
requests: 5,
})
}).throws(
'Unsupported database "better-sqlite3". The limiter can only work with PostgreSQL and MySQL databases'
'Unsupported database "libsql". The limiter can only work with PostgreSQL, MySQL, and SQLite databases'
)

test('define readonly properties', async ({ assert }) => {
Expand Down Expand Up @@ -209,7 +209,10 @@ test.group('Limiter database store | wrapper | consume', () => {
try {
await store.consume('ip_localhost')
} catch (error) {
assert.match(error.message, /relation "foo" does not exist|Table 'limiter.foo' doesn't exist/)
assert.match(
error.message,
/relation "foo" does not exist|Table 'limiter.foo' doesn't exist|no such table: foo/
)
}
})
})
Expand Down