diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..e1e9941 --- /dev/null +++ b/index.ts @@ -0,0 +1,15 @@ +/* + * @adonisjs/limiter + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * as errors from './src/errors.js' +export { Limiter } from './src/limiter.js' +export { LimiterResponse } from './src/response.js' +export { HttpLimiter } from './src/http_limiter.js' +export { LimiterManager } from './src/limiter_manager.js' +export { defineConfig, stores } from './src/define_config.js' diff --git a/package.json b/package.json index 6d680eb..eb1d247 100644 --- a/package.json +++ b/package.json @@ -5,22 +5,12 @@ "main": "build/index.js", "type": "module", "files": [ - "build/src", - "build/services", - "build/providers", - "build/commands", - "build/factories", - "build/stubs", - "build/configure.js", - "build/configure.d.ts", - "build/index.d.ts", - "build/index.js" + "build" ], "exports": { ".": "./build/index.js", "./limiter_provider": "./build/providers/limiter_provider.js", "./services/main": "./build/services/main.js", - "./throttle_middleware": "./build/src/throttle_middleware.js", "./types": "./build/src/types.js" }, "scripts": { diff --git a/providers/limiter_provider.ts b/providers/limiter_provider.ts new file mode 100644 index 0000000..29e6388 --- /dev/null +++ b/providers/limiter_provider.ts @@ -0,0 +1,43 @@ +/* + * @adonisjs/limiter + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { configProvider } from '@adonisjs/core' +import { ApplicationService } from '@adonisjs/core/types' +import { RuntimeException } from '@adonisjs/core/exceptions' + +import { LimiterManager } from '../index.js' +import type { LimiterService } from '../src/types.js' + +declare module '@adonisjs/core/types' { + export interface ContainerBindings { + 'limiter.manager': LimiterService + } +} + +export default class LimiterProvider { + constructor(protected app: ApplicationService) {} + + register() { + this.app.container.singleton('limiter.manager', async () => { + const limiterConfigProvider = this.app.config.get('limiter', {}) + + /** + * Resolve config from the provider + */ + const config = await configProvider.resolve(this.app, limiterConfigProvider) + if (!config) { + throw new RuntimeException( + 'Invalid "config/limiter.ts" file. Make sure you are using the "defineConfig" method' + ) + } + + return new LimiterManager(config) + }) + } +} diff --git a/services/main.ts b/services/main.ts new file mode 100644 index 0000000..df6eefa --- /dev/null +++ b/services/main.ts @@ -0,0 +1,23 @@ +/* + * @adonisjs/limiter + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from '@adonisjs/core/services/app' +import { LimiterService } from '../src/types.js' + +let limiter: LimiterService + +/** + * Returns a singleton instance of the LimiterManager class from the + * container. + */ +await app.booted(async () => { + limiter = await app.container.make('limiter.manager') +}) + +export { limiter as default } diff --git a/src/types.ts b/src/types.ts index 231675d..a4cdeee 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,8 @@ * file that was distributed with this source code. */ +import { ConfigProvider } from '@adonisjs/core/types' +import { LimiterManager } from './limiter_manager.js' import type { LimiterResponse } from './response.js' /** @@ -220,3 +222,25 @@ export interface LimiterStoreContract { export type LimiterManagerStoreFactory = ( options: LimiterConsumptionOptions ) => LimiterStoreContract + +/** + * A list of known limiters inferred from the user config + */ +export interface LimitersList {} + +/** + * Helper method to resolve configured limiters + * inside user app + */ +export type InferLimiters< + T extends ConfigProvider<{ stores: Record }>, +> = Awaited>['stores'] + +/** + * Limiter service is a singleton instance of limiter + * manager configured using user app's config + */ +export interface LimiterService + extends LimiterManager< + LimitersList extends Record ? LimitersList : never + > {} diff --git a/tests/limiter_provider.spec.ts b/tests/limiter_provider.spec.ts new file mode 100644 index 0000000..2775f26 --- /dev/null +++ b/tests/limiter_provider.spec.ts @@ -0,0 +1,88 @@ +/* + * @adonisjs/limiter + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { IgnitorFactory } from '@adonisjs/core/factories' +import type { RedisService } from '@adonisjs/redis/types' + +import { createRedis } from './helpers.js' +import { LimiterManager, defineConfig, stores } from '../index.js' + +const BASE_URL = new URL('./tmp/', import.meta.url) +const IMPORTER = (filePath: string) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, BASE_URL).href) + } + return import(filePath) +} + +test.group('Limiter provider', () => { + test('register limiter provider', async ({ assert }) => { + const redis = createRedis() as unknown as RedisService + + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: [() => import('../providers/limiter_provider.js')], + }, + }) + .withCoreConfig() + .withCoreProviders() + .merge({ + config: { + limiter: defineConfig({ + default: 'redis', + stores: { + redis: stores.redis({ + connectionName: 'local', + }), + }, + }), + }, + }) + .create(BASE_URL, { + importer: IMPORTER, + }) + + const app = ignitor.createApp('web') + await app.init() + app.container.singleton('redis', () => redis) + await app.boot() + + assert.instanceOf(await app.container.make('limiter.manager'), LimiterManager) + }) + + test('throw error when config is invalid', async () => { + const redis = createRedis() as unknown as RedisService + + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: [() => import('../providers/limiter_provider.js')], + }, + }) + .withCoreConfig() + .withCoreProviders() + .merge({ + config: { + limiter: {}, + }, + }) + .create(BASE_URL, { + importer: IMPORTER, + }) + + const app = ignitor.createApp('web') + await app.init() + app.container.singleton('redis', () => redis) + await app.boot() + + await app.container.make('limiter.manager') + }).throws('Invalid "config/limiter.ts" file. Make sure you are using the "defineConfig" method') +})