├── .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 |
--------------------------------------------------------------------------------