diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4e8e60f..7b91fe4 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -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 diff --git a/package.json b/package.json index 8573236..94d0cdc 100644 --- a/package.json +++ b/package.json @@ -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", "clean": "del-cli build", "typecheck": "tsc --noEmit", "copy:templates": "copyfiles \"stubs/**/*.stub\" --up=\"1\" build", @@ -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", @@ -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", diff --git a/src/stores/database.ts b/src/stores/database.ts index 1bc7acf..7f86eb6 100644 --- a/src/stores/database.ts +++ b/src/stores/database.ts @@ -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' @@ -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` ) } @@ -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 } } diff --git a/tests/define_config.spec.ts b/tests/define_config.spec.ts index fea520e..f628e88 100644 --- a/tests/define_config.spec.ts +++ b/tests/define_config.spec.ts @@ -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() + expectTypeOf(limiter.use).returns.toEqualTypeOf() assert.isNull( await limiter.use('redis', { duration: '1 min', requests: 5 }).get('ip_localhost') diff --git a/tests/helpers.ts b/tests/helpers.ts index 36d3692..3d56028 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -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: { diff --git a/tests/stores/database.spec.ts b/tests/stores/database.spec.ts index 96df7ba..f25fc1c 100644 --- a/tests/stores/database.spec.ts +++ b/tests/stores/database.spec.ts @@ -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 }) => { @@ -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/ + ) } }) })