├── .editorconfig ├── .github ├── funding.yml └── workflows │ ├── checks.yml │ ├── labels.yml │ ├── release.yml │ ├── stale.yml │ ├── test_mysql.yml │ ├── test_postgres.yml │ └── test_redis.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── LICENSE.md ├── README.md ├── bin └── test.ts ├── commands └── cache_clear.ts ├── compose.yml ├── configure.ts ├── eslint.config.js ├── index.ts ├── package.json ├── providers └── cache_provider.ts ├── services └── main.ts ├── src ├── bindings │ ├── edge.ts │ └── repl.ts ├── debug.ts ├── define_config.ts ├── drivers.ts ├── store.ts └── types.ts ├── stubs ├── config.stub └── main.ts ├── tests ├── commands │ └── cache_clear.spec.ts ├── configure.spec.ts ├── database.spec.ts ├── helpers.ts ├── provider.spec.ts └── redis.spec.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = ignore 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [Julien-R44, thetutlage] 2 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | - workflow_call 7 | 8 | jobs: 9 | lint: 10 | uses: adonisjs/.github/.github/workflows/lint.yml@main 11 | 12 | typecheck: 13 | uses: adonisjs/.github/.github/workflows/typecheck.yml@main 14 | 15 | test_redis: 16 | uses: adonisjs/cache/.github/workflows/test_redis.yml@1.x 17 | 18 | test_postgres: 19 | uses: adonisjs/cache/.github/workflows/test_postgres.yml@1.x 20 | 21 | test_mysql: 22 | uses: adonisjs/cache/.github/workflows/test_mysql.yml@1.x 23 | -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync labels 2 | on: 3 | workflow_dispatch: 4 | permissions: 5 | issues: write 6 | jobs: 7 | labels: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: EndBug/label-sync@v2 12 | with: 13 | config-file: 'https://raw.githubusercontent.com/thetutlage/static/main/labels.yml' 14 | delete-other-labels: true 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: workflow_dispatch 3 | 4 | permissions: 5 | contents: write 6 | id-token: write 7 | 8 | jobs: 9 | checks: 10 | uses: ./.github/workflows/checks.yml 11 | release: 12 | needs: checks 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | - name: git config 22 | run: | 23 | git config user.name "${GITHUB_ACTOR}" 24 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 25 | - name: Init npm config 26 | run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 27 | env: 28 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | - run: npm install 30 | - run: npm run release -- --ci 31 | env: 32 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 0 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v9 11 | with: 12 | stale-issue-message: 'This issue has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still need help on this issue' 13 | stale-pr-message: 'This pull request has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still intend to submit this pull request' 14 | close-issue-message: 'This issue has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still need help on this issue' 15 | close-pr-message: 'This pull request has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still intend to submit this pull request' 16 | days-before-stale: 21 17 | days-before-close: 5 18 | -------------------------------------------------------------------------------- /.github/workflows/test_mysql.yml: -------------------------------------------------------------------------------- 1 | name: Test MySQL 2 | 3 | on: 4 | - workflow_call 5 | 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 10 10 | strategy: 11 | matrix: 12 | node-version: ["lts/iron", "lts/jod", "latest"] 13 | redis-version: ["7-alpine" ] 14 | postgres-version: ["17-alpine"] 15 | mysql-version: ["5.7", "8", "latest"] 16 | 17 | services: 18 | redis: 19 | image: redis:${{ matrix.redis-version }} 20 | ports: 21 | - 6379:6379 22 | postgres: 23 | image: postgres:${{ matrix.postgres-version }} 24 | env: 25 | POSTGRES_USER: postgres 26 | POSTGRES_PASSWORD: postgres 27 | POSTGRES_DB: postgres 28 | ports: 29 | - 5432:5432 30 | mysql: 31 | image: mysql:${{ matrix.mysql-version }} 32 | env: 33 | MYSQL_ROOT_PASSWORD: root 34 | MYSQL_DATABASE: mysql 35 | ports: 36 | - 3306:3306 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - name: Setup Node.js 42 | uses: actions/setup-node@v4 43 | with: 44 | node-version: ${{ matrix.node-version }} 45 | 46 | - name: Install 47 | run: npm install 48 | 49 | - name: Test 50 | run: FORCE_COLOR=1 npm test 51 | -------------------------------------------------------------------------------- /.github/workflows/test_postgres.yml: -------------------------------------------------------------------------------- 1 | name: Test Postgres 2 | 3 | on: 4 | - workflow_call 5 | 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 10 10 | strategy: 11 | matrix: 12 | node-version: ["lts/iron", "lts/jod", "latest"] 13 | redis-version: ["7-alpine"] 14 | postgres-version: [ "15-alpine", "16-alpine", "17-alpine"] 15 | mysql-version: ["latest"] 16 | 17 | services: 18 | redis: 19 | image: redis:${{ matrix.redis-version }} 20 | ports: 21 | - 6379:6379 22 | postgres: 23 | image: postgres:${{ matrix.postgres-version }} 24 | env: 25 | POSTGRES_USER: postgres 26 | POSTGRES_PASSWORD: postgres 27 | POSTGRES_DB: postgres 28 | ports: 29 | - 5432:5432 30 | mysql: 31 | image: mysql:${{ matrix.mysql-version }} 32 | env: 33 | MYSQL_ROOT_PASSWORD: root 34 | MYSQL_DATABASE: mysql 35 | ports: 36 | - 3306:3306 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - name: Setup Node.js 42 | uses: actions/setup-node@v4 43 | with: 44 | node-version: ${{ matrix.node-version }} 45 | 46 | - name: Install 47 | run: npm install 48 | 49 | - name: Test 50 | run: FORCE_COLOR=1 npm test 51 | -------------------------------------------------------------------------------- /.github/workflows/test_redis.yml: -------------------------------------------------------------------------------- 1 | name: Test MySQL 2 | 3 | on: 4 | - workflow_call 5 | 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 10 10 | strategy: 11 | matrix: 12 | node-version: ["lts/iron", "lts/jod", "latest"] 13 | redis-version: ["6-alpine", "7-alpine"] 14 | postgres-version: ["17-alpine"] 15 | mysql-version: ["latest"] 16 | 17 | services: 18 | redis: 19 | image: redis:${{ matrix.redis-version }} 20 | ports: 21 | - 6379:6379 22 | postgres: 23 | image: postgres:${{ matrix.postgres-version }} 24 | env: 25 | POSTGRES_USER: postgres 26 | POSTGRES_PASSWORD: postgres 27 | POSTGRES_DB: postgres 28 | ports: 29 | - 5432:5432 30 | mysql: 31 | image: mysql:${{ matrix.mysql-version }} 32 | env: 33 | MYSQL_ROOT_PASSWORD: root 34 | MYSQL_DATABASE: mysql 35 | ports: 36 | - 3306:3306 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - name: Setup Node.js 42 | uses: actions/setup-node@v4 43 | with: 44 | node-version: ${{ matrix.node-version }} 45 | 46 | - name: Install 47 | run: npm install 48 | 49 | - name: Test 50 | run: FORCE_COLOR=1 npm test 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_STORE 4 | .nyc_output 5 | .idea 6 | .vscode/ 7 | *.sublime-project 8 | *.sublime-workspace 9 | *.log 10 | build 11 | dist 12 | yarn.lock 13 | shrinkwrap.yaml 14 | package-lock.json 15 | test/__app 16 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | coverage 4 | *.html 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright (c) 2023 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @adonisjs/cache 2 | 3 |
4 | 5 | [![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] 6 | 7 | ## Introduction 8 | Cache module for AdonisJS built on top of [Bentocache](https://github.com/Julien-R44/bentocache). Support multiples drivers, File, In-Memory, Redis, SQLs databases and more. 9 | 10 | ## Official Documentation 11 | The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/digging-deeper/cache). 12 | 13 | ## Contributing 14 | One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. 15 | 16 | We encourage you to read the [contribution guide](https://github.com/adonisjs/.github/blob/main/docs/CONTRIBUTING.md) before contributing to the framework. 17 | 18 | ## Code of Conduct 19 | In order to ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md). 20 | 21 | ## License 22 | AdonisJS Cache is open-sourced software licensed under the [MIT license](LICENSE.md). 23 | 24 | [gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/cache/checks.yml?branch=1.x&style=for-the-badge 25 | [gh-workflow-url]: https://github.com/adonisjs/cache/actions/workflows/checks.yml "Github action" 26 | 27 | [npm-image]: https://img.shields.io/npm/v/@adonisjs/cache/latest.svg?style=for-the-badge&logo=npm 28 | [npm-url]: https://www.npmjs.com/package/@adonisjs/cache/v/latest "npm" 29 | 30 | [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript 31 | 32 | [license-url]: LICENSE.md 33 | [license-image]: https://img.shields.io/github/license/adonisjs/cache?style=for-the-badge 34 | -------------------------------------------------------------------------------- /bin/test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from '@japa/assert' 2 | import { snapshot } from '@japa/snapshot' 3 | import { fileSystem } from '@japa/file-system' 4 | import { expectTypeOf } from '@japa/expect-type' 5 | import { processCLIArgs, configure, run } from '@japa/runner' 6 | 7 | /* 8 | |-------------------------------------------------------------------------- 9 | | Configure tests 10 | |-------------------------------------------------------------------------- 11 | | 12 | | The configure method accepts the configuration to configure the Japa 13 | | tests runner. 14 | | 15 | | The first method call "processCLIArgs" process the command line arguments 16 | | and turns them into a config object. Using this method is not mandatory. 17 | | 18 | | Please consult japa.dev/runner-config for the config docs. 19 | */ 20 | processCLIArgs(process.argv.slice(2)) 21 | configure({ 22 | files: ['tests/**/*.spec.ts'], 23 | plugins: [assert(), snapshot(), fileSystem({ autoClean: true }), expectTypeOf()], 24 | }) 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Run tests 29 | |-------------------------------------------------------------------------- 30 | | 31 | | The following "run" method is required to execute all the tests. 32 | | 33 | */ 34 | run() 35 | -------------------------------------------------------------------------------- /commands/cache_clear.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { args, BaseCommand, flags } from '@adonisjs/core/ace' 11 | 12 | import { CacheService } from '../src/types.js' 13 | import { CommandOptions } from '@adonisjs/core/types/ace' 14 | 15 | export default class CacheClear extends BaseCommand { 16 | static commandName = 'cache:clear' 17 | static description = 'Clear the application cache' 18 | static options: CommandOptions = { 19 | startApp: true, 20 | } 21 | 22 | /** 23 | * Choose a custom cache store to clear. Otherwise, we use the 24 | * default one 25 | */ 26 | @args.string({ description: 'Define a custom cache store to clear', required: false }) 27 | declare store: string 28 | 29 | /** 30 | * Optionally select a namespace to clear. Defaults to the whole cache. 31 | */ 32 | @flags.string({ description: 'Select a cache namespace to clear', alias: 'n' }) 33 | declare namespace: string 34 | 35 | /** 36 | * Prompts to take consent when clearing the cache in production 37 | */ 38 | async #takeProductionConsent(): Promise { 39 | const question = 'You are in production environment. Want to continue clearing the cache?' 40 | try { 41 | return await this.prompt.confirm(question) 42 | } catch (error) { 43 | return false 44 | } 45 | } 46 | 47 | /** 48 | * Check if the given cache exist 49 | */ 50 | #cacheExists(cache: CacheService, cacheName: string) { 51 | try { 52 | cache.use(cacheName) 53 | return true 54 | } catch (error) { 55 | return false 56 | } 57 | } 58 | 59 | /** 60 | * Handle command 61 | */ 62 | async run() { 63 | const cache = await this.app.container.make('cache.manager') 64 | this.store = this.store || cache.defaultStoreName 65 | 66 | /** 67 | * Exit if cache store doesn't exist 68 | */ 69 | if (!this.#cacheExists(cache, this.store)) { 70 | this.logger.error( 71 | `"${this.store}" is not a valid cache store. Double check config/cache.ts file` 72 | ) 73 | this.exitCode = 1 74 | return 75 | } 76 | 77 | /** 78 | * Take consent when clearing the cache in production 79 | */ 80 | if (this.app.inProduction) { 81 | const shouldClear = await this.#takeProductionConsent() 82 | if (!shouldClear) return 83 | } 84 | 85 | /** 86 | * Finally clear the cache 87 | */ 88 | const cacheHandler = cache.use(this.store) 89 | if (this.namespace) { 90 | await cacheHandler.namespace(this.namespace).clear() 91 | this.logger.success( 92 | `Cleared namespace "${this.namespace}" for "${this.store}" cache successfully` 93 | ) 94 | } else { 95 | await cacheHandler.clear() 96 | this.logger.success(`Cleared "${this.store}" cache successfully`) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis:6.2-alpine 4 | restart: always 5 | ports: 6 | - '6379:6379' 7 | 8 | postgres: 9 | image: postgres:15-alpine 10 | restart: always 11 | environment: 12 | POSTGRES_USER: postgres 13 | POSTGRES_PASSWORD: postgres 14 | POSTGRES_DB: postgres 15 | ports: 16 | - '5432:5432' 17 | 18 | mysql-legacy: 19 | image: mysql:5.7 20 | restart: always 21 | environment: 22 | MYSQL_ROOT_PASSWORD: root 23 | MYSQL_DATABASE: mysql 24 | ports: 25 | - '3307:3306' 26 | 27 | mysql: 28 | image: mysql:8.0 29 | restart: always 30 | environment: 31 | MYSQL_ROOT_PASSWORD: root 32 | MYSQL_DATABASE: mysql 33 | ports: 34 | - '3306:3306' 35 | -------------------------------------------------------------------------------- /configure.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type Configure from '@adonisjs/core/commands/configure' 11 | 12 | import { stubsRoot } from './stubs/main.js' 13 | 14 | const DRIVERS = ['redis', 'file', 'database', 'dynamodb'] as const 15 | const DRIVERS_INFO: { 16 | [K in (typeof DRIVERS)[number]]: { 17 | envVars?: Record 18 | envValidations?: Record 19 | } 20 | } = { 21 | file: {}, 22 | redis: {}, 23 | database: {}, 24 | dynamodb: { 25 | envValidations: { 26 | AWS_ACCESS_KEY_ID: `Env.schema.string()`, 27 | AWS_SECRET_ACCESS_KEY: `Env.schema.string()`, 28 | AWS_REGION: `Env.schema.string()`, 29 | DYNAMODB_ENDPOINT: `Env.schema.string()`, 30 | }, 31 | envVars: { 32 | AWS_ACCESS_KEY_ID: '', 33 | AWS_SECRET_ACCESS_KEY: '', 34 | AWS_REGION: '', 35 | DYNAMODB_ENDPOINT: '', 36 | }, 37 | }, 38 | } 39 | 40 | /** 41 | * Configures the package 42 | */ 43 | export async function configure(command: Configure) { 44 | const driver = await command.prompt.choice( 45 | 'Select the cache driver you plan to use', 46 | ['redis', 'file', 'database', 'dynamodb'], 47 | { 48 | hint: 'You can always change it later', 49 | } 50 | ) 51 | 52 | const codemods = await command.createCodemods() 53 | 54 | /** 55 | * Publish provider 56 | */ 57 | await codemods.updateRcFile((rcFile) => { 58 | rcFile.addProvider('@adonisjs/cache/cache_provider').addCommand('@adonisjs/cache/commands') 59 | }) 60 | 61 | const { envVars, envValidations } = DRIVERS_INFO[driver] 62 | 63 | /** 64 | * Define environment variables 65 | */ 66 | if (envVars) { 67 | await codemods.defineEnvVariables(envVars) 68 | } 69 | 70 | /** 71 | * Define environment validations 72 | */ 73 | if (envValidations) { 74 | await codemods.defineEnvValidations({ variables: envValidations }) 75 | } 76 | 77 | /** 78 | * Publish config 79 | */ 80 | await codemods.makeUsingStub(stubsRoot, 'config.stub', { driver: driver }) 81 | } 82 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { configPkg } from '@adonisjs/eslint-config' 2 | 3 | export default configPkg({ 4 | ignores: ['coverage'], 5 | }) 6 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | export * from 'bentocache' 11 | 12 | export { store } from './src/store.js' 13 | export { configure } from './configure.js' 14 | export { defineConfig } from './src/define_config.js' 15 | export { drivers } from './src/drivers.js' 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adonisjs/cache", 3 | "description": "Official caching module for AdonisJS framework", 4 | "version": "1.1.3", 5 | "engines": { 6 | "node": ">=20.6.0" 7 | }, 8 | "main": "build/index.js", 9 | "type": "module", 10 | "files": [ 11 | "build", 12 | "!build/bin", 13 | "!build/tests" 14 | ], 15 | "exports": { 16 | ".": "./build/index.js", 17 | "./types": "./build/src/types.js", 18 | "./commands/*": "./build/commands/*.js", 19 | "./commands": "./build/commands/main.js", 20 | "./services/main": "./build/services/main.js", 21 | "./cache_provider": "./build/providers/cache_provider.js" 22 | }, 23 | "scripts": { 24 | "clean": "del-cli build", 25 | "copy:templates": "copyfiles --up 1 \"stubs/**/*.stub\" build", 26 | "typecheck": "tsc --noEmit", 27 | "index:commands": "adonis-kit index build/commands", 28 | "lint": "eslint", 29 | "format": "prettier --write .", 30 | "quick:test": "node --enable-source-maps --import=ts-node-maintained/register/esm bin/test.ts", 31 | "pretest": "npm run lint", 32 | "test": "c8 npm run quick:test", 33 | "prebuild": "npm run lint && npm run clean", 34 | "build": "tsup-node --metafile && tsc --emitDeclarationOnly --declaration", 35 | "postbuild": "npm run copy:templates && npm run index:commands", 36 | "release": "npx release-it", 37 | "version": "npm run build", 38 | "prepublishOnly": "npm run build" 39 | }, 40 | "devDependencies": { 41 | "@adonisjs/assembler": "^7.8.2", 42 | "@adonisjs/core": "^6.17.2", 43 | "@adonisjs/eslint-config": "^2.0.0", 44 | "@adonisjs/lucid": "^21.6.0", 45 | "@adonisjs/prettier-config": "^1.4.0", 46 | "@adonisjs/redis": "^9.2.0", 47 | "@adonisjs/tsconfig": "^1.4.0", 48 | "@japa/assert": "^4.0.1", 49 | "@japa/expect-type": "^2.0.3", 50 | "@japa/file-system": "^2.3.2", 51 | "@japa/runner": "^4.2.0", 52 | "@japa/snapshot": "^2.0.8", 53 | "@release-it/conventional-changelog": "^10.0.0", 54 | "@swc/core": "^1.10.16", 55 | "@types/node": "~20.17.19", 56 | "better-sqlite3": "^11.8.1", 57 | "c8": "^10.1.3", 58 | "copyfiles": "^2.4.1", 59 | "del-cli": "^6.0.0", 60 | "edge.js": "^6.2.1", 61 | "eslint": "^9.20.1", 62 | "ioredis": "^5.5.0", 63 | "knex": "^3.1.0", 64 | "luxon": "^3.5.0", 65 | "mysql2": "^3.12.0", 66 | "p-event": "^6.0.1", 67 | "pg": "^8.13.3", 68 | "prettier": "^3.5.1", 69 | "release-it": "^18.1.2", 70 | "ts-node-maintained": "^10.9.5", 71 | "tsup": "^8.3.6", 72 | "typescript": "~5.7.3" 73 | }, 74 | "dependencies": { 75 | "bentocache": "^1.2.0" 76 | }, 77 | "peerDependencies": { 78 | "@adonisjs/assembler": "^7.0.0", 79 | "@adonisjs/core": "^6.2.0", 80 | "@adonisjs/lucid": "^20.0.0 || ^21.0.0", 81 | "@adonisjs/redis": "^8.0.0 || ^9.0.0" 82 | }, 83 | "peerDependenciesMeta": { 84 | "@adonisjs/redis": { 85 | "optional": true 86 | }, 87 | "@adonisjs/lucid": { 88 | "optional": true 89 | } 90 | }, 91 | "author": "Julien Ripouteau ", 92 | "contributors": [ 93 | "Romain Lanz " 94 | ], 95 | "license": "MIT", 96 | "keywords": [ 97 | "adonisjs", 98 | "cache", 99 | "caching", 100 | "bentocache" 101 | ], 102 | "homepage": "https://github.com/adonisjs/cache#readme", 103 | "repository": { 104 | "type": "git", 105 | "url": "git+https://github.com/adonisjs/cache.git" 106 | }, 107 | "bugs": { 108 | "url": "https://github.com/adonisjs/cache/issues" 109 | }, 110 | "publishConfig": { 111 | "provenance": true, 112 | "access": "public" 113 | }, 114 | "prettier": "@adonisjs/prettier-config", 115 | "release-it": { 116 | "git": { 117 | "requireCleanWorkingDir": true, 118 | "requireUpstream": true, 119 | "commitMessage": "chore(release): ${version}", 120 | "tagAnnotation": "v${version}", 121 | "push": true, 122 | "tagName": "v${version}" 123 | }, 124 | "github": { 125 | "release": true 126 | }, 127 | "npm": { 128 | "publish": true, 129 | "skipChecks": true 130 | }, 131 | "plugins": { 132 | "@release-it/conventional-changelog": { 133 | "preset": { 134 | "name": "angular" 135 | } 136 | } 137 | } 138 | }, 139 | "c8": { 140 | "reporter": [ 141 | "text", 142 | "html" 143 | ], 144 | "exclude": [ 145 | "tests/**" 146 | ] 147 | }, 148 | "tsup": { 149 | "entry": [ 150 | "index.ts", 151 | "src/types.ts", 152 | "providers/cache_provider.ts", 153 | "services/main.ts", 154 | "commands/cache_clear.ts" 155 | ], 156 | "outDir": "./build", 157 | "clean": true, 158 | "format": "esm", 159 | "dts": false, 160 | "sourcemap": true, 161 | "target": "esnext" 162 | }, 163 | "packageManager": "pnpm@10.4.0" 164 | } 165 | -------------------------------------------------------------------------------- /providers/cache_provider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { ApplicationService } from '@adonisjs/core/types' 11 | 12 | import { defineConfig } from '../index.js' 13 | import type { CacheEvents } from 'bentocache/types' 14 | import type { CacheService } from '../src/types.js' 15 | import { defineReplBindings } from '../src/bindings/repl.js' 16 | import { registerViewBindings } from '../src/bindings/edge.js' 17 | 18 | /** 19 | * Extend Adonis.js types to include cache 20 | */ 21 | declare module '@adonisjs/core/types' { 22 | /** 23 | * Adding cache type to the application container 24 | */ 25 | export interface ContainerBindings { 26 | 'cache.manager': CacheService 27 | } 28 | 29 | /** 30 | * Add cache events to the application events list 31 | */ 32 | export interface EventsList { 33 | 'cache:cleared': CacheEvents['cache:cleared'] 34 | 'cache:deleted': CacheEvents['cache:deleted'] 35 | 'cache:hit': CacheEvents['cache:hit'] 36 | 'cache:miss': CacheEvents['cache:miss'] 37 | 'cache:written': CacheEvents['cache:written'] 38 | 'bus:message:published': CacheEvents['bus:message:published'] 39 | 'bus:message:received': CacheEvents['bus:message:received'] 40 | } 41 | } 42 | 43 | /** 44 | * Cache provider to register cache specific bindings 45 | */ 46 | export default class CacheProvider { 47 | constructor(protected app: ApplicationService) {} 48 | 49 | /** 50 | * Register the cache manager to the container 51 | */ 52 | async #registerCacheManager() { 53 | const cacheConfig = this.app.config.get>('cache') 54 | 55 | this.app.container.singleton('cache.manager', async () => { 56 | const { BentoCache } = await import('bentocache') 57 | const emitter = await this.app.container.make('emitter') 58 | 59 | /** 60 | * Resolve all store config providers 61 | */ 62 | const resolvedStores = Object.entries(cacheConfig.stores).map(async ([name, store]) => { 63 | return [name, await store.entry().resolver(this.app)] 64 | }) 65 | 66 | return new BentoCache({ 67 | ...cacheConfig, 68 | emitter: emitter as any, 69 | default: cacheConfig.default, 70 | stores: Object.fromEntries(await Promise.all(resolvedStores)), 71 | }) 72 | }) 73 | } 74 | 75 | /** 76 | * Register REPL bindings 77 | */ 78 | async #registerReplBindings() { 79 | if (this.app.getEnvironment() !== 'repl') { 80 | return 81 | } 82 | 83 | const repl = await this.app.container.make('repl') 84 | defineReplBindings(this.app, repl) 85 | } 86 | 87 | /** 88 | * Register edge bindings 89 | */ 90 | async #registerEdgeBindings() { 91 | if (!this.app.usingEdgeJS) return 92 | 93 | const manager = await this.app.container.make('cache.manager') 94 | await registerViewBindings(manager) 95 | } 96 | 97 | /** 98 | * Register bindings 99 | */ 100 | async register() { 101 | await this.#registerCacheManager() 102 | await this.#registerReplBindings() 103 | await this.#registerEdgeBindings() 104 | } 105 | 106 | /** 107 | * Gracefully shutdown connections when app goes down 108 | */ 109 | async shutdown() { 110 | try { 111 | const cache = await this.app.container.make('cache.manager') 112 | await cache.disconnectAll() 113 | } catch (_e) { 114 | // Ignore errors 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /services/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { CacheService } from '../src/types.js' 11 | import app from '@adonisjs/core/services/app' 12 | 13 | let cache: CacheService 14 | 15 | /** 16 | * Returns a singleton instance of the Cache manager 17 | */ 18 | await app.booted(async () => { 19 | cache = await app.container.make('cache.manager') 20 | }) 21 | 22 | export { cache as default } 23 | -------------------------------------------------------------------------------- /src/bindings/edge.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import debug from '../debug.js' 11 | import { CacheService } from '../types.js' 12 | 13 | export async function registerViewBindings(manager: CacheService) { 14 | const edge = await import('edge.js') 15 | debug('detected edge installation. Registering cache global helpers') 16 | 17 | edge.default.global('cache', manager) 18 | } 19 | -------------------------------------------------------------------------------- /src/bindings/repl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { Repl } from '@adonisjs/core/repl' 11 | import type { ApplicationService } from '@adonisjs/core/types' 12 | 13 | /** 14 | * Helper to define REPL state 15 | */ 16 | function setupReplState(repl: any, key: string, value: any) { 17 | repl.server.context[key] = value 18 | repl.notify( 19 | `Loaded ${key} module. You can access it using the "${repl.colors.underline(key)}" variable` 20 | ) 21 | } 22 | 23 | /** 24 | * Define REPL bindings 25 | */ 26 | export function defineReplBindings(app: ApplicationService, Repl: Repl) { 27 | /** 28 | * Load cache provider to the cache property 29 | */ 30 | Repl.addMethod( 31 | 'loadCache', 32 | async (repl) => setupReplState(repl, 'cache', await app.container.make('cache.manager')), 33 | { description: 'Load cache provider to the "cache" property' } 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/debug.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/drive 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { debuglog } from 'node:util' 11 | 12 | export default debuglog('adonisjs:cache') 13 | -------------------------------------------------------------------------------- /src/define_config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { Store } from './store.js' 11 | import { CacheOptions } from './types.js' 12 | 13 | /** 14 | * Define cache configuration 15 | */ 16 | export function defineConfig>( 17 | config: CacheOptions & { 18 | default: keyof KnownCaches 19 | stores: KnownCaches 20 | } 21 | ) { 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /src/drivers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /// 11 | /// 12 | 13 | import { configProvider } from '@adonisjs/core' 14 | import type { RedisConnection } from '@adonisjs/redis' 15 | import type { ConfigProvider } from '@adonisjs/core/types' 16 | import type { RedisConnections } from '@adonisjs/redis/types' 17 | import { 18 | MemoryConfig, 19 | CreateDriverResult, 20 | L1CacheDriver, 21 | L2CacheDriver, 22 | CreateBusDriverResult, 23 | DynamoDBConfig, 24 | FileConfig, 25 | KyselyConfig, 26 | OrchidConfig, 27 | } from 'bentocache/types' 28 | import { RuntimeException } from '@adonisjs/core/exceptions' 29 | 30 | /** 31 | * Different drivers supported by the cache module 32 | */ 33 | export const drivers: { 34 | memory: (config?: MemoryConfig) => ConfigProvider> 35 | redis: (config: { 36 | connectionName?: keyof RedisConnections 37 | }) => ConfigProvider> 38 | redisBus: (config: { 39 | connectionName?: keyof RedisConnections 40 | }) => ConfigProvider 41 | database: (config?: { 42 | connectionName?: string 43 | }) => ConfigProvider> 44 | dynamodb: (config: DynamoDBConfig) => ConfigProvider> 45 | file: (config: FileConfig) => ConfigProvider> 46 | kysely: (config: KyselyConfig) => ConfigProvider> 47 | orchid: (config: OrchidConfig) => ConfigProvider> 48 | } = { 49 | /** 50 | * Redis driver for L2 layer 51 | * You must install @adonisjs/redis to use this driver 52 | */ 53 | redis(config) { 54 | return configProvider.create(async (app) => { 55 | const redis = await app.container.make('redis') 56 | const { redisDriver } = await import('bentocache/drivers/redis') 57 | 58 | const redisConnection = redis.connection(config.connectionName) as any as RedisConnection 59 | return redisDriver({ connection: redisConnection.ioConnection }) 60 | }) 61 | }, 62 | 63 | /** 64 | * Redis driver for the sync bus 65 | * You must install @adonisjs/redis to use this driver 66 | */ 67 | redisBus(config) { 68 | return configProvider.create(async (app) => { 69 | const redis = await app.container.make('redis') 70 | const { redisBusDriver } = await import('bentocache/drivers/redis') 71 | 72 | const redisConnection = redis.connection(config.connectionName) as any as RedisConnection 73 | return redisBusDriver({ connection: redisConnection.ioConnection.options }) 74 | }) 75 | }, 76 | 77 | /** 78 | * Memory driver for L1 layer 79 | */ 80 | memory(config) { 81 | return configProvider.create(async () => { 82 | const { memoryDriver } = await import('bentocache/drivers/memory') 83 | return memoryDriver(config) 84 | }) 85 | }, 86 | 87 | /** 88 | * Database driver for L2 layer 89 | * You must install @adonisjs/lucid to use this driver 90 | */ 91 | database(config) { 92 | return configProvider.create(async (app) => { 93 | const db = await app.container.make('lucid.db') 94 | const connectionName = config?.connectionName || db.primaryConnectionName 95 | const connection = db.manager.get(connectionName) 96 | 97 | /** 98 | * Throw error when mentioned connection is not specified 99 | * in the database file 100 | */ 101 | if (!connection) { 102 | throw new RuntimeException( 103 | `Invalid connection name "${connectionName}" referenced by "config/cache.ts" file. First register the connection inside "config/database.ts" file` 104 | ) 105 | } 106 | 107 | const { knexDriver } = await import('bentocache/drivers/knex') 108 | return knexDriver({ connection: db.connection(connectionName).getWriteClient() }) 109 | }) 110 | }, 111 | 112 | /** 113 | * DynamoDB driver for L2 layer 114 | * You must install @aws-sdk/client-dynamodb to use this driver 115 | */ 116 | dynamodb(config) { 117 | return configProvider.create(async () => { 118 | const { dynamoDbDriver } = await import('bentocache/drivers/dynamodb') 119 | return dynamoDbDriver(config) 120 | }) 121 | }, 122 | 123 | /** 124 | * File driver for L2 layer 125 | */ 126 | file(config) { 127 | return configProvider.create(async () => { 128 | const { fileDriver } = await import('bentocache/drivers/file') 129 | return fileDriver(config) 130 | }) 131 | }, 132 | 133 | /** 134 | * Kysely driver for L2 layer 135 | */ 136 | kysely(config) { 137 | return configProvider.create(async () => { 138 | const { kyselyDriver } = await import('bentocache/drivers/kysely') 139 | return kyselyDriver(config) 140 | }) 141 | }, 142 | 143 | /** 144 | * Orchid driver for L2 layer 145 | */ 146 | orchid(config) { 147 | return configProvider.create(async () => { 148 | const { orchidDriver } = await import('bentocache/drivers/orchid') 149 | return orchidDriver(config) 150 | }) 151 | }, 152 | } 153 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { bentostore } from 'bentocache' 11 | import { configProvider } from '@adonisjs/core' 12 | import type { ConfigProvider } from '@adonisjs/core/types' 13 | import { 14 | RawCommonOptions, 15 | CreateDriverResult, 16 | L1CacheDriver, 17 | CreateBusDriverResult, 18 | L2CacheDriver, 19 | } from 'bentocache/types' 20 | 21 | /** 22 | * Create a new store 23 | */ 24 | export function store(options?: RawCommonOptions & { prefix?: string }) { 25 | return new Store(options) 26 | } 27 | 28 | export class Store { 29 | #baseOptions: RawCommonOptions & { prefix?: string } = {} 30 | #l1?: ConfigProvider> 31 | #l2?: ConfigProvider> 32 | #bus?: ConfigProvider 33 | 34 | constructor(baseOptions: RawCommonOptions & { prefix?: string } = {}) { 35 | this.#baseOptions = baseOptions 36 | } 37 | 38 | /** 39 | * Add a L1 layer to your store. This is usually a memory driver 40 | * for fast access purposes. 41 | */ 42 | useL1Layer(driver: ConfigProvider>) { 43 | this.#l1 = driver 44 | return this 45 | } 46 | 47 | /** 48 | * Add a L2 layer to your store. This is usually something 49 | * distributed like Redis, DynamoDB, Sql database, etc. 50 | */ 51 | useL2Layer(driver: ConfigProvider>) { 52 | this.#l2 = driver 53 | return this 54 | } 55 | 56 | /** 57 | * Add a bus to your store. It will be used to synchronize L1 layers between 58 | * different instances of your application. 59 | */ 60 | useBus(bus: ConfigProvider) { 61 | this.#bus = bus 62 | return this 63 | } 64 | 65 | /** 66 | * Create a config provider for the store 67 | */ 68 | entry() { 69 | return configProvider.create(async (app) => { 70 | const storeInstance = bentostore(this.#baseOptions) 71 | 72 | if (this.#l1) storeInstance.useL1Layer(await this.#l1?.resolver(app)) 73 | if (this.#l2) storeInstance.useL2Layer(await this.#l2?.resolver(app)) 74 | if (this.#bus) storeInstance.useBus(await this.#bus?.resolver(app)) 75 | 76 | return storeInstance 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | export * from 'bentocache/types' 11 | 12 | import type { BentoCache, bentostore } from 'bentocache' 13 | import type { RawBentoCacheOptions } from 'bentocache/types' 14 | 15 | import type { store } from './store.js' 16 | 17 | /** 18 | * The options accepted by the cache module 19 | */ 20 | export type CacheOptions = Omit 21 | 22 | /** 23 | * Infer the stores from the user config 24 | */ 25 | export type InferStores> }> = { 26 | [K in keyof T['stores']]: any 27 | } 28 | 29 | /** 30 | * A list of known caches stores inferred from the user config 31 | * This interface must be extended in user-land 32 | */ 33 | export interface CacheStores {} 34 | 35 | /** 36 | * The cache service interface registered with the container 37 | */ 38 | export interface CacheService 39 | extends BentoCache< 40 | CacheStores extends Record> ? CacheStores : never 41 | > {} 42 | -------------------------------------------------------------------------------- /stubs/config.stub: -------------------------------------------------------------------------------- 1 | {{{ 2 | exports({ to: app.configPath('cache.ts') }) 3 | }}} 4 | import env from '#start/env' 5 | {{#if driver === 'file'}} 6 | import app from '@adonisjs/core/services/app' 7 | {{/if}} 8 | import { defineConfig, store, drivers } from '@adonisjs/cache' 9 | 10 | const cacheConfig = defineConfig({ 11 | default: 'default', 12 | 13 | stores: { 14 | memoryOnly: store().useL1Layer(drivers.memory()), 15 | 16 | default: store() 17 | .useL1Layer(drivers.memory()) 18 | {{#if driver === 'database'}} 19 | .useL2Layer(drivers.database({ 20 | connectionName: 'primary', 21 | })) 22 | {{#elif driver === 'redis'}} 23 | .useL2Layer(drivers.redis({ 24 | connectionName: 'main', 25 | })) 26 | {{#elif driver === 'file'}} 27 | .useL2Layer(drivers.file({ 28 | directory: app.tmpPath('cache') 29 | })) 30 | {{#elif driver === 'dynamodb'}} 31 | .useL2Layer(drivers.dynamodb({ 32 | table: { name: 'cache' }, 33 | credentials: { 34 | accessKeyId: env.get('AWS_ACCESS_KEY_ID'), 35 | secretAccessKey: env.get('AWS_SECRET_ACCESS_KEY') 36 | }, 37 | region: env.get('AWS_REGION'), 38 | endpoint: env.get('DYNAMODB_ENDPOINT') 39 | })) 40 | {{/if}} 41 | } 42 | }) 43 | 44 | export default cacheConfig 45 | 46 | declare module '@adonisjs/cache/types' { 47 | interface CacheStores extends InferStores {} 48 | } 49 | -------------------------------------------------------------------------------- /stubs/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { dirname } from 'node:path' 11 | import { fileURLToPath } from 'node:url' 12 | 13 | export const stubsRoot = dirname(fileURLToPath(import.meta.url)) 14 | -------------------------------------------------------------------------------- /tests/commands/cache_clear.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { AceFactory } from '@adonisjs/core/factories' 12 | import CacheClear from '../../commands/cache_clear.js' 13 | import { getCacheService } from '../helpers.js' 14 | 15 | test.group('CacheClear', () => { 16 | test('Clear default cache', async ({ fs, assert }) => { 17 | const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} }) 18 | await ace.app.init() 19 | 20 | const cache = getCacheService() 21 | ace.app.container.singleton('cache.manager', () => cache) 22 | ace.ui.switchMode('raw') 23 | 24 | await cache.set({ key: 'foo', value: 'bar' }) 25 | assert.equal(await cache.get({ key: 'foo' }), 'bar') 26 | 27 | const command = await ace.create(CacheClear, []) 28 | await command.run() 29 | 30 | assert.isUndefined(await cache.get({ key: 'foo' })) 31 | 32 | command.assertLog(`[ green(success) ] Cleared "${cache.defaultStoreName}" cache successfully`) 33 | }) 34 | 35 | test('Clear selected cache', async ({ fs, assert }) => { 36 | const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} }) 37 | await ace.app.init() 38 | 39 | const cache = getCacheService() 40 | ace.app.container.singleton('cache.manager', () => cache) 41 | ace.ui.switchMode('raw') 42 | 43 | const memoryStore = cache.use('memory') 44 | await memoryStore.set({ key: 'foo', value: 'bar' }) 45 | assert.equal(await memoryStore.get({ key: 'foo' }), 'bar') 46 | 47 | const command = await ace.create(CacheClear, []) 48 | command.store = 'memory' 49 | await command.run() 50 | 51 | assert.isUndefined(await memoryStore.get({ key: 'foo' })) 52 | 53 | command.assertLog(`[ green(success) ] Cleared "memory" cache successfully`) 54 | }) 55 | 56 | test('ask for confirmation when clearing cache in production', async ({ 57 | fs, 58 | assert, 59 | cleanup, 60 | }) => { 61 | process.env.NODE_ENV = 'production' 62 | cleanup(() => { 63 | delete process.env.NODE_ENV 64 | }) 65 | 66 | const ace = await new AceFactory().make(fs.baseUrl, { 67 | importer: (path) => import(path), 68 | }) 69 | 70 | await ace.app.init().then(() => ace.app.boot()) 71 | ace.ui.switchMode('raw') 72 | 73 | const cache = getCacheService() 74 | ace.app.container.singleton('cache.manager', () => cache) 75 | 76 | await cache.set({ key: 'foo', value: 'bar' }) 77 | cleanup(() => cache.clear()) 78 | 79 | const command = await ace.create(CacheClear, []) 80 | command.prompt 81 | .trap('You are in production environment. Want to continue clearing the cache?') 82 | .reject() 83 | 84 | await command.run() 85 | 86 | assert.equal(await cache.get({ key: 'foo' }), 'bar') 87 | }) 88 | 89 | test('clear cache when user confirms production prompt', async ({ fs, assert, cleanup }) => { 90 | process.env.NODE_ENV = 'production' 91 | cleanup(() => { 92 | delete process.env.NODE_ENV 93 | }) 94 | 95 | const ace = await new AceFactory().make(fs.baseUrl, { 96 | importer: (path) => import(path), 97 | }) 98 | 99 | await ace.app.init().then(() => ace.app.boot()) 100 | ace.ui.switchMode('raw') 101 | 102 | const cache = getCacheService() 103 | ace.app.container.singleton('cache.manager', () => cache) 104 | 105 | await cache.set({ key: 'foo', value: 'bar' }) 106 | cleanup(() => cache.clear()) 107 | 108 | const command = await ace.create(CacheClear, []) 109 | command.prompt 110 | .trap('You are in production environment. Want to continue clearing the cache?') 111 | .accept() 112 | 113 | await command.run() 114 | 115 | assert.isUndefined(await cache.get({ key: 'foo' })) 116 | }) 117 | 118 | test('exit when user specify a non-existing cache store', async ({ fs, assert }) => { 119 | const ace = await new AceFactory().make(fs.baseUrl, { 120 | importer: (path) => import(path), 121 | }) 122 | 123 | await ace.app.init().then(() => ace.app.boot()) 124 | ace.ui.switchMode('raw') 125 | 126 | const cache = getCacheService() 127 | ace.app.container.singleton('cache.manager', () => cache) 128 | 129 | const command = await ace.create(CacheClear, []) 130 | command.store = 'foo' 131 | 132 | await command.run() 133 | 134 | command.assertLog( 135 | `[ red(error) ] "foo" is not a valid cache store. Double check config/cache.ts file` 136 | ) 137 | assert.equal(command.exitCode, 1) 138 | }) 139 | 140 | test('Clear a specific namespace in the default cache', async ({ fs, assert }) => { 141 | const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} }) 142 | await ace.app.init() 143 | 144 | const cache = getCacheService() 145 | ace.app.container.singleton('cache.manager', () => cache) 146 | ace.ui.switchMode('raw') 147 | 148 | await cache.set({ key: 'foo', value: 'bar' }) 149 | assert.equal(await cache.get({ key: 'foo' }), 'bar') 150 | 151 | await cache.namespace('users').set({ key: 'foo', value: 'bar' }) 152 | assert.equal(await cache.namespace('users').get({ key: 'foo' }), 'bar') 153 | 154 | const command = await ace.create(CacheClear, []) 155 | command.namespace = 'users' 156 | await command.run() 157 | 158 | assert.equal(await cache.get({ key: 'foo' }), 'bar') 159 | assert.isUndefined(await cache.namespace('users').get({ key: 'foo' })) 160 | 161 | command.assertLog( 162 | `[ green(success) ] Cleared namespace "users" for "${cache.defaultStoreName}" cache successfully` 163 | ) 164 | }) 165 | }) 166 | -------------------------------------------------------------------------------- /tests/configure.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { fileURLToPath } from 'node:url' 12 | import { IgnitorFactory } from '@adonisjs/core/factories' 13 | import Configure from '@adonisjs/core/commands/configure' 14 | 15 | const BASE_URL = new URL('./tmp/', import.meta.url) 16 | 17 | test.group('Configure', (group) => { 18 | group.tap((t) => t.timeout(10_000)) 19 | 20 | group.each.setup(async ({ context }) => { 21 | context.fs.baseUrl = BASE_URL 22 | context.fs.basePath = fileURLToPath(BASE_URL) 23 | 24 | await context.fs.create('.env', '') 25 | await context.fs.createJson('tsconfig.json', {}) 26 | await context.fs.create('start/env.ts', `export default Env.create(new URL('./'), {})`) 27 | await context.fs.create('adonisrc.ts', `export default defineConfig({})`) 28 | }) 29 | 30 | test('add commands and provider', async ({ assert }) => { 31 | const ignitor = new IgnitorFactory() 32 | .withCoreProviders() 33 | .withCoreConfig() 34 | .create(BASE_URL, { 35 | importer: (filePath) => { 36 | if (filePath.startsWith('./') || filePath.startsWith('../')) { 37 | return import(new URL(filePath, BASE_URL).href) 38 | } 39 | 40 | return import(filePath) 41 | }, 42 | }) 43 | 44 | const app = ignitor.createApp('web') 45 | await app.init().then(() => app.boot()) 46 | 47 | const ace = await app.container.make('ace') 48 | ace.prompt.trap('Select the cache driver you plan to use').chooseOption(0) 49 | ace.ui.switchMode('raw') 50 | 51 | const command = await ace.create(Configure, ['../../index.js']) 52 | await command.exec() 53 | 54 | await assert.fileExists('config/cache.ts') 55 | await assert.fileExists('adonisrc.ts') 56 | await assert.fileContains('adonisrc.ts', '@adonisjs/cache/commands') 57 | await assert.fileContains('adonisrc.ts', '@adonisjs/cache/cache_provider') 58 | }) 59 | 60 | test('create redis cache', async ({ assert }) => { 61 | const ignitor = new IgnitorFactory() 62 | .withCoreProviders() 63 | .withCoreConfig() 64 | .create(BASE_URL, { 65 | importer: (filePath) => { 66 | if (filePath.startsWith('./') || filePath.startsWith('../')) { 67 | return import(new URL(filePath, BASE_URL).href) 68 | } 69 | 70 | return import(filePath) 71 | }, 72 | }) 73 | 74 | const app = ignitor.createApp('web') 75 | await app.init().then(() => app.boot()) 76 | 77 | const ace = await app.container.make('ace') 78 | ace.prompt.trap('Select the cache driver you plan to use').chooseOption(0) 79 | ace.ui.switchMode('raw') 80 | 81 | const command = await ace.create(Configure, ['../../index.js']) 82 | await command.exec() 83 | 84 | await assert.fileContains('config/cache.ts', 'defineConfig({') 85 | }) 86 | 87 | test('create dynamo cache', async ({ assert }) => { 88 | const ignitor = new IgnitorFactory() 89 | .withCoreProviders() 90 | .withCoreConfig() 91 | .create(BASE_URL, { 92 | importer: (filePath) => { 93 | if (filePath.startsWith('./') || filePath.startsWith('../')) { 94 | return import(new URL(filePath, BASE_URL).href) 95 | } 96 | 97 | return import(filePath) 98 | }, 99 | }) 100 | 101 | const app = ignitor.createApp('web') 102 | await app.init().then(() => app.boot()) 103 | 104 | const ace = await app.container.make('ace') 105 | ace.prompt.trap('Select the cache driver you plan to use').chooseOption(3) 106 | ace.ui.switchMode('raw') 107 | 108 | const command = await ace.create(Configure, ['../../index.js']) 109 | await command.exec() 110 | 111 | await assert.fileContains('config/cache.ts', 'defineConfig({') 112 | 113 | await assert.fileContains('.env', 'AWS_ACCESS_KEY_ID') 114 | await assert.fileContains('.env', 'AWS_SECRET_ACCESS_KEY') 115 | await assert.fileContains('.env', 'AWS_REGION') 116 | await assert.fileContains('.env', 'DYNAMODB_ENDPOINT') 117 | 118 | await assert.fileContains('start/env.ts', 'AWS_ACCESS_KEY_ID: Env.schema.string()') 119 | await assert.fileContains('start/env.ts', 'AWS_SECRET_ACCESS_KEY: Env.schema.string()') 120 | await assert.fileContains('start/env.ts', 'AWS_REGION: Env.schema.string()') 121 | await assert.fileContains('start/env.ts', 'DYNAMODB_ENDPOINT: Env.schema.string()') 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /tests/database.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { join } from 'node:path' 11 | import { test } from '@japa/runner' 12 | import { defineConfig as defineLucidConfig } from '@adonisjs/lucid' 13 | 14 | import { setupApp } from './helpers.js' 15 | import { defineConfig, drivers, store } from '../index.js' 16 | 17 | test.group('Database', () => { 18 | test('use database defined connection', async ({ assert, fs }) => { 19 | await fs.create('foo', '') 20 | 21 | const app = await setupApp('web', { 22 | database: defineLucidConfig({ 23 | connection: 'sqlite', 24 | connections: { 25 | sqlite: { 26 | client: 'better-sqlite3', 27 | connection: { filename: join(fs.basePath, 'db.sqlite3') }, 28 | useNullAsDefault: true, 29 | }, 30 | sqlite2: { 31 | client: 'better-sqlite3', 32 | connection: { filename: join(fs.basePath, 'db2.sqlite3') }, 33 | useNullAsDefault: true, 34 | }, 35 | }, 36 | }), 37 | cache: defineConfig({ 38 | default: 'sqlite', 39 | stores: { 40 | sqlite: store().useL2Layer(drivers.database({ connectionName: 'sqlite2' })), 41 | }, 42 | }), 43 | }) 44 | 45 | const db = await app.container.make('lucid.db') 46 | const cache = await app.container.make('cache.manager') 47 | 48 | await cache.set({ key: 'foo', value: 'bar' }) 49 | 50 | const r1 = await db 51 | .connection('sqlite2') 52 | .from('bentocache') 53 | .select('*') 54 | .where('key', 'bentocache:foo') 55 | .firstOrFail() 56 | 57 | const r2 = await cache.get({ key: 'foo' }) 58 | 59 | assert.deepEqual(JSON.parse(r1.value).value, 'bar') 60 | assert.equal(r2, 'bar') 61 | }) 62 | 63 | test('use default database connection if not defined', async ({ assert, fs }) => { 64 | await fs.create('foo', '') 65 | 66 | const app = await setupApp('web', { 67 | database: defineLucidConfig({ 68 | connection: 'sqlite', 69 | connections: { 70 | sqlite: { 71 | client: 'better-sqlite3', 72 | connection: { filename: join(fs.basePath, 'db.sqlite3') }, 73 | useNullAsDefault: true, 74 | }, 75 | sqlite2: { 76 | client: 'better-sqlite3', 77 | connection: { filename: join(fs.basePath, 'db2.sqlite3') }, 78 | useNullAsDefault: true, 79 | }, 80 | }, 81 | }), 82 | cache: defineConfig({ 83 | default: 'sqlite', 84 | stores: { 85 | sqlite: store().useL2Layer(drivers.database()), 86 | }, 87 | }), 88 | }) 89 | 90 | const db = await app.container.make('lucid.db') 91 | const cache = await app.container.make('cache.manager') 92 | 93 | await cache.set({ key: 'foo', value: 'bar' }) 94 | 95 | const r1 = await db 96 | .connection('sqlite') 97 | .from('bentocache') 98 | .select('*') 99 | .where('key', 'bentocache:foo') 100 | .firstOrFail() 101 | 102 | assert.deepEqual(JSON.parse(r1.value).value, 'bar') 103 | }) 104 | 105 | test('{$i} - test {client}') 106 | .with([ 107 | { 108 | client: 'mysql2', 109 | connection: { 110 | host: 'localhost', 111 | port: 3306, 112 | user: 'root', 113 | password: 'root', 114 | database: 'mysql', 115 | }, 116 | }, 117 | { 118 | client: 'pg', 119 | connection: { 120 | host: 'localhost', 121 | port: 5432, 122 | user: 'postgres', 123 | password: 'postgres', 124 | database: 'postgres', 125 | }, 126 | }, 127 | { 128 | client: 'postgres', 129 | connection: { 130 | host: 'localhost', 131 | port: 5432, 132 | user: 'postgres', 133 | password: 'postgres', 134 | database: 'postgres', 135 | }, 136 | }, 137 | ]) 138 | .run(async ({ assert, fs }, data) => { 139 | await fs.create('foo', '') 140 | 141 | const app = await setupApp('web', { 142 | database: defineLucidConfig({ 143 | // @ts-expect-error tkt 144 | connections: { db: data }, 145 | }), 146 | cache: defineConfig({ 147 | default: 'database', 148 | stores: { 149 | database: store().useL2Layer(drivers.database({ connectionName: 'db' })), 150 | }, 151 | }), 152 | }) 153 | 154 | const db = await app.container.make('lucid.db') 155 | const cache = await app.container.make('cache.manager') 156 | 157 | await cache.set({ key: 'foo', value: 'bar' }) 158 | 159 | const r1 = await db 160 | .connection('db') 161 | .from('bentocache') 162 | .select('*') 163 | .where('key', 'bentocache:foo') 164 | .firstOrFail() 165 | 166 | assert.deepEqual(JSON.parse(r1.value).value, 'bar') 167 | }) 168 | }) 169 | -------------------------------------------------------------------------------- /tests/helpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { getActiveTest } from '@japa/runner' 11 | import { BentoCache, bentostore } from 'bentocache' 12 | import { redisDriver } from 'bentocache/drivers/redis' 13 | import { memoryDriver } from 'bentocache/drivers/memory' 14 | import { IgnitorFactory } from '@adonisjs/core/factories' 15 | import { AppEnvironments } from '@adonisjs/core/types/app' 16 | import { defineConfig as defineRedisConfig } from '@adonisjs/redis' 17 | import { defineConfig as defineLucidConfig } from '@adonisjs/lucid' 18 | 19 | import { defineConfig, store, drivers } from '../index.js' 20 | 21 | export function getCacheService(config?: any): BentoCache { 22 | const defaultConfig = { 23 | default: 'redis' as const, 24 | stores: { 25 | redis: bentostore().useL2Layer( 26 | redisDriver({ connection: { host: '127.0.0.1', port: 6379 } }) 27 | ), 28 | 29 | memory: bentostore().useL1Layer(memoryDriver({})), 30 | }, 31 | } 32 | 33 | const bentocache = new BentoCache(defaultConfig || config) 34 | const test = getActiveTest() 35 | test?.cleanup(() => bentocache.disconnectAll()) 36 | 37 | return bentocache 38 | } 39 | 40 | const BASE_URL = new URL('./tmp/', import.meta.url) 41 | 42 | export async function setupApp( 43 | env?: AppEnvironments, 44 | config: { 45 | database?: ReturnType 46 | redis?: ReturnType 47 | cache?: ReturnType 48 | } = {} 49 | ) { 50 | const ignitor = new IgnitorFactory() 51 | .withCoreProviders() 52 | .withCoreConfig() 53 | .merge({ 54 | config: { 55 | database: 56 | config.database || 57 | defineLucidConfig({ 58 | connection: 'sqlite', 59 | connections: { 60 | sqlite: { 61 | client: 'better-sqlite3', 62 | connection: { filename: 'db.sqlite3' }, 63 | useNullAsDefault: true, 64 | }, 65 | }, 66 | }), 67 | redis: 68 | config.redis || 69 | defineRedisConfig({ 70 | connection: 'local', 71 | connections: { local: { host: '127.0.0.1', port: 6379 } }, 72 | }), 73 | cache: 74 | config.cache || 75 | defineConfig({ 76 | default: 'memory', 77 | stores: { 78 | memory: store().useL1Layer(drivers.memory({})), 79 | redis: store().useL2Layer(drivers.redis({ connectionName: 'local' as any })), 80 | }, 81 | }), 82 | }, 83 | rcFileContents: { 84 | providers: [ 85 | () => import('@adonisjs/redis/redis_provider'), 86 | () => import('@adonisjs/lucid/database_provider'), 87 | () => import('../providers/cache_provider.js'), 88 | ], 89 | }, 90 | }) 91 | .create(BASE_URL, { 92 | importer: (filePath) => { 93 | if (filePath.startsWith('./') || filePath.startsWith('../')) { 94 | return import(new URL(filePath, BASE_URL).href) 95 | } 96 | 97 | return import(filePath) 98 | }, 99 | }) 100 | 101 | const app = ignitor.createApp(env || 'web') 102 | await app.init().then(() => app.boot()) 103 | 104 | getActiveTest()?.cleanup(() => app.terminate()) 105 | 106 | return app 107 | } 108 | -------------------------------------------------------------------------------- /tests/provider.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { pEvent } from 'p-event' 11 | import { test } from '@japa/runner' 12 | 13 | import { setupApp } from './helpers.js' 14 | 15 | test.group('Provider', () => { 16 | test('app emitter should be binded to bentocache', async ({ assert }) => { 17 | const app = await setupApp() 18 | 19 | const cache = await app.container.make('cache.manager') 20 | const emitter = await app.container.make('emitter') 21 | 22 | cache.set({ key: 'foo', value: 'bar' }) 23 | 24 | const event = await pEvent(emitter, 'cache:written') 25 | assert.deepEqual(event, { key: 'foo', value: 'bar', store: 'memory' }) 26 | }) 27 | 28 | test('app emitter should be extended with events types', async ({ expectTypeOf }) => { 29 | const app = await setupApp() 30 | const emitter = await app.container.make('emitter') 31 | 32 | const onProperty = expectTypeOf(emitter).toHaveProperty('on') 33 | onProperty.toBeCallableWith('cache:cleared', () => {}) 34 | onProperty.toBeCallableWith('cache:written', () => {}) 35 | onProperty.toBeCallableWith('cache:deleted', () => {}) 36 | }) 37 | 38 | test('should disconnect stores when shutting down the app', async () => { 39 | const app = await setupApp() 40 | const cache = await app.container.make('cache.manager') 41 | 42 | await cache.use('redis').get({ key: 'foo' }) 43 | 44 | await app.terminate() 45 | }) 46 | 47 | test('register repl bindings', async ({ assert }) => { 48 | const app = await setupApp('repl') 49 | 50 | const repl = await app.container.make('repl') 51 | const replMethods = repl.getMethods() 52 | 53 | assert.property(replMethods, 'loadCache') 54 | assert.isFunction(replMethods.loadCache.handler) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /tests/redis.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/cache 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { setTimeout } from 'node:timers/promises' 12 | import { defineConfig as defineRedisConfig } from '@adonisjs/redis' 13 | 14 | import { setupApp } from './helpers.js' 15 | import { defineConfig, drivers, store } from '../index.js' 16 | 17 | test.group('Redis', () => { 18 | test('re-use adonisjs/redis connection instance', async ({ assert }) => { 19 | const app = await setupApp('web', { 20 | redis: defineRedisConfig({ 21 | connection: 'local', 22 | connections: { local: { host: '127.0.0.1', port: 6379 } }, 23 | }), 24 | cache: defineConfig({ 25 | default: 'redis', 26 | stores: { 27 | redis: store().useL2Layer(drivers.redis({ connectionName: 'local' as any })), 28 | }, 29 | }), 30 | }) 31 | 32 | const redis = await app.container.make('redis') 33 | const cache = await app.container.make('cache.manager') 34 | 35 | await cache.set({ key: 'foo', value: 'bar' }) 36 | await redis.set('bentocache:foo', 'bar') 37 | 38 | const result = await redis.info('clients') 39 | const connectedClients = result.split('\n')[1].split(':')[1].trim() 40 | 41 | assert.equal(connectedClients, '1') 42 | }) 43 | 44 | test('by default it should prefix the keys', async ({ assert }) => { 45 | const app = await setupApp('web', { 46 | redis: defineRedisConfig({ 47 | connection: 'local', 48 | connections: { local: { host: '127.0.0.1', port: 6379 } }, 49 | }), 50 | cache: defineConfig({ 51 | default: 'redis', 52 | stores: { 53 | redis: store().useL2Layer(drivers.redis({ connectionName: 'local' as any })), 54 | }, 55 | }), 56 | }) 57 | 58 | const redis = await app.container.make('redis') 59 | const cache = await app.container.make('cache.manager') 60 | 61 | await cache.set({ key: 'foo', value: 'bar' }) 62 | 63 | const result = await redis.get('bentocache:foo') 64 | 65 | assert.isDefined(result) 66 | assert.equal(JSON.parse(result!).value, 'bar') 67 | }) 68 | 69 | test('bus should works fine', async ({ assert }) => { 70 | assert.plan(2) 71 | 72 | const config = { 73 | redis: defineRedisConfig({ 74 | connection: 'local', 75 | connections: { local: { host: '127.0.0.1', port: 6379 } }, 76 | }), 77 | cache: defineConfig({ 78 | default: 'redis', 79 | stores: { 80 | redis: store() 81 | .useL1Layer(drivers.memory({})) 82 | .useL2Layer(drivers.redis({ connectionName: 'local' as any })) 83 | .useBus(drivers.redisBus({ connectionName: 'local' as any })), 84 | }, 85 | }), 86 | } 87 | const app = await setupApp('web', config) 88 | 89 | const redis = await app.container.make('redis') 90 | const cache = await app.container.make('cache.manager') 91 | 92 | redis.subscribe('bentocache.notifications:redis', () => assert.isTrue(true)) 93 | 94 | await setTimeout(200) 95 | await cache.set({ key: 'foo', value: 'bar' }) 96 | await cache.delete({ key: 'foo' }) 97 | await setTimeout(200) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@adonisjs/tsconfig/tsconfig.package.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "outDir": "./build", 6 | } 7 | } 8 | --------------------------------------------------------------------------------