diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6fbbc64 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +REDIS_HOST= +REDIS_PORT= +REDIS_CLUSTER_PORTS= diff --git a/.github/COMMIT_CONVENTION.md b/.github/COMMIT_CONVENTION.md deleted file mode 100644 index f6ea85e..0000000 --- a/.github/COMMIT_CONVENTION.md +++ /dev/null @@ -1,69 +0,0 @@ -## Git Commit Message Convention - -> This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). - -Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. - -``` js -/^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ -``` - -## Commit Message Format -A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: - -> The **scope** is optional - -``` -feat(router): add support for prefix - -Prefix makes it easier to append a path to a group of routes -``` - -1. `feat` is type. -2. `router` is scope and is optional -3. `add support for prefix` is the subject -4. The **body** is followed by a blank line. -5. The optional **footer** can be added after the body, followed by a blank line. - -## Types -Only one type can be used at a time and only following types are allowed. - -- feat -- fix -- docs -- style -- refactor -- perf -- test -- workflow -- ci -- chore -- types -- build - -If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. - -### Revert -If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. - -## Scope -The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. - -## Subject -The subject contains succinct description of the change: - -- use the imperative, present tense: "change" not "changed" nor "changes". -- don't capitalize first letter -- no dot (.) at the end - -## Body - -Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". -The body should include the motivation for the change and contrast this with previous behavior. - -## Footer - -The footer should contain any information about **Breaking Changes** and is also the place to -reference GitHub issues that this commit **Closes**. - -**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index f0c5446..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributing - -AdonisJS is a community driven project. You are free to contribute in any of the following ways. - -- [Coding style](coding-style) -- [Fix bugs by creating PR's](fix-bugs-by-creating-prs) -- [Share an RFC for new features or big changes](share-an-rfc-for-new-features-or-big-changes) -- [Report security issues](report-security-issues) -- [Be a part of the community](be-a-part-of-community) - -## Coding style - -Majority of AdonisJS core packages are written in Typescript. Having a brief knowledge of Typescript is required to contribute to the core. - -## Fix bugs by creating PR's - -We appreciate every time you report a bug in the framework or related libraries. However, taking time to submit a PR can help us in fixing bugs quickly and ensure a healthy and stable eco-system. - -Go through the following points, before creating a new PR. - -1. Create an issue discussing the bug or short-coming in the framework. -2. Once approved, go ahead and fork the REPO. -3. Make sure to start from the `develop`, since this is the upto date branch. -4. Make sure to keep commits small and relevant. -5. We follow [conventional-commits](https://github.com/conventional-changelog/conventional-changelog) to structure our commit messages. Instead of running `git commit`, you must run `npm commit`, which will show you prompts to create a valid commit message. -6. Once done with all the changes, create a PR against the `develop` branch. - -## Share an RFC for new features or big changes - -Sharing PR's for small changes works great. However, when contributing big features to the framework, it is required to go through the RFC process. - -### What is an RFC? - -RFC stands for **Request for Commits**, a standard process followed by many other frameworks including [Ember](https://github.com/emberjs/rfcs), [yarn](https://github.com/yarnpkg/rfcs) and [rust](https://github.com/rust-lang/rfcs). - -In brief, RFC process allows you to talk about the changes with everyone in the community and get a view of the core team before dedicating your time to work on the feature. - -The RFC proposals are created as Pull Request on [adonisjs/rfcs](https://github.com/adonisjs/rfcs) repo. Make sure to read the README to learn about the process in depth. - -## Report security issues - -All of the security issues, must be reported via [email](mailto:virk@adonisjs.com) and not using any of the public channels. - -## Be a part of community - -We welcome you to participate in [GitHub Discussion](https://github.com/adonisjs/core/discussions) and the AdonisJS [Discord Server](https://discord.gg/vDcEjq6). You are free to ask your questions and share your work or contributions made to AdonisJS eco-system. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index e65000c..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug report -about: Report identified bugs ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -- Lots of raised issues are directly not bugs but instead are design decisions taken by us. -- Make use of our [GH discussions](https://github.com/adonisjs/core/discussions), or [discord server](https://discord.me/adonisjs), if you are not sure that you are reporting a bug. -- Ensure the issue isn't already reported. -- Ensure you are reporting the bug in the correct repo. - -*Delete the above section and the instructions in the sections below before submitting* - -## Package version - - -## Node.js and npm version - - -## Sample Code (to reproduce the issue) - - -## BONUS (a sample repo to reproduce the issue) - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index abd44a5..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Feature request -about: Propose changes for adding a new feature ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -## Consider an RFC - -Please create an [RFC](https://github.com/adonisjs/rfcs) instead, if - -- Feature introduces a breaking change -- Demands lots of time and changes in the current code base. - -*Delete the above section and the instructions in the sections below before submitting* - -## Why this feature is required (specific use-cases will be appreciated)? - - -## Have you tried any other work arounds? - - -## Are you willing to work on it with little guidance? - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 5c8d18f..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,28 +0,0 @@ - - -## Proposed changes - -Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. - -## Types of changes - -What types of changes does your code introduce? - -_Put an `x` in the boxes that apply_ - -- [ ] Bugfix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - -## Checklist - -_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ - -- [ ] I have read the [CONTRIBUTING](https://github.com/adonisjs/redis/blob/master/.github/CONTRIBUTING.md) doc -- [ ] Lint and unit tests pass locally with my changes -- [ ] I have added tests that prove my fix is effective or that my feature works. -- [ ] I have added necessary documentation (if appropriate) - -## Further comments - -If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 0000000..ba001c6 --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,170 @@ +[ + { + "name": "Priority: Critical", + "color": "ea0056", + "description": "The issue needs urgent attention", + "aliases": [] + }, + { + "name": "Priority: High", + "color": "5666ed", + "description": "Look into this issue before picking up any new work", + "aliases": [] + }, + { + "name": "Priority: Medium", + "color": "f4ff61", + "description": "Try to fix the issue for the next patch/minor release", + "aliases": [] + }, + { + "name": "Priority: Low", + "color": "87dfd6", + "description": "Something worth considering, but not a top priority for the team", + "aliases": [] + }, + { + "name": "Semver: Alpha", + "color": "008480", + "description": "Will make it's way to the next alpha version of the package", + "aliases": [] + }, + { + "name": "Semver: Major", + "color": "ea0056", + "description": "Has breaking changes", + "aliases": [] + }, + { + "name": "Semver: Minor", + "color": "fbe555", + "description": "Mainly new features and improvements", + "aliases": [] + }, + { + "name": "Semver: Next", + "color": "5666ed", + "description": "Will make it's way to the bleeding edge version of the package", + "aliases": [] + }, + { + "name": "Semver: Patch", + "color": "87dfd6", + "description": "A bug fix", + "aliases": [] + }, + { + "name": "Status: Abandoned", + "color": "ffffff", + "description": "Dropped and not into consideration", + "aliases": ["wontfix"] + }, + { + "name": "Status: Accepted", + "color": "e5fbf2", + "description": "The proposal or the feature has been accepted for the future versions", + "aliases": [] + }, + { + "name": "Status: Blocked", + "color": "ea0056", + "description": "The work on the issue or the PR is blocked. Check comments for reasoning", + "aliases": [] + }, + { + "name": "Status: Completed", + "color": "008672", + "description": "The work has been completed, but not released yet", + "aliases": [] + }, + { + "name": "Status: In Progress", + "color": "73dbc4", + "description": "Still banging the keyboard", + "aliases": ["in progress"] + }, + { + "name": "Status: On Hold", + "color": "f4ff61", + "description": "The work was started earlier, but is on hold now. Check comments for reasoning", + "aliases": ["On Hold"] + }, + { + "name": "Status: Review Needed", + "color": "fbe555", + "description": "Review from the core team is required before moving forward", + "aliases": [] + }, + { + "name": "Status: Awaiting More Information", + "color": "89f8ce", + "description": "Waiting on the issue reporter or PR author to provide more information", + "aliases": [] + }, + { + "name": "Status: Need Contributors", + "color": "7057ff", + "description": "Looking for contributors to help us move forward with this issue or PR", + "aliases": [] + }, + { + "name": "Type: Bug", + "color": "ea0056", + "description": "The issue has indentified a bug", + "aliases": ["bug"] + }, + { + "name": "Type: Security", + "color": "ea0056", + "description": "Spotted security vulnerability and is a top priority for the core team", + "aliases": [] + }, + { + "name": "Type: Duplicate", + "color": "00837e", + "description": "Already answered or fixed previously", + "aliases": ["duplicate"] + }, + { + "name": "Type: Enhancement", + "color": "89f8ce", + "description": "Improving an existing feature", + "aliases": ["enhancement"] + }, + { + "name": "Type: Feature Request", + "color": "483add", + "description": "Request to add a new feature to the package", + "aliases": [] + }, + { + "name": "Type: Invalid", + "color": "dbdbdb", + "description": "Doesn't really belong here. Maybe use discussion threads?", + "aliases": ["invalid"] + }, + { + "name": "Type: Question", + "color": "eceafc", + "description": "Needs clarification", + "aliases": ["help wanted", "question"] + }, + { + "name": "Type: Documentation Change", + "color": "7057ff", + "description": "Documentation needs some improvements", + "aliases": ["documentation"] + }, + { + "name": "Type: Dependencies Update", + "color": "00837e", + "description": "Bump dependencies", + "aliases": ["dependencies"] + }, + { + "name": "Good First Issue", + "color": "008480", + "description": "Want to contribute? Just filter by this label", + "aliases": ["good first issue"] + } +] diff --git a/.github/stale.yml b/.github/stale.yml index 7a6a571..f767674 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -6,10 +6,10 @@ daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - - "Type: Security" + - 'Type: Security' # Label to use when marking an issue as stale -staleLabel: "Status: Abandoned" +staleLabel: 'Status: Abandoned' # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b778ca..de4467a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,15 +8,18 @@ jobs: strategy: matrix: node-version: - - 14.15.4 - 17.x + - 20.x steps: - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + - name: Install run: npm install + - name: Run tests run: npm test diff --git a/.gitignore b/.gitignore index 8cdf144..dc0a88c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ package-lock.json yarn.lock shrinkwrap.yaml test/__app +.env diff --git a/.husky/commit-msg b/.husky/commit-msg index 4654c12..988eb59 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,3 +1,4 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" -HUSKY_GIT_PARAMS=$1 node ./node_modules/@adonisjs/mrm-preset/validate-commit/conventional/validate.js +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no -- commitlint --edit diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 225554a..0000000 --- a/.npmignore +++ /dev/null @@ -1,12 +0,0 @@ -coverage -node_modules -.DS_Store -npm-debug.log -test -.travis.yml -.editorconfig -benchmarks -.idea -bin -out -.nyc_output diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 38c5e38..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM node:14-alpine as build-deps - -RUN apk update && apk upgrade && \ - apk add --update git && \ - apk add --update openssh && \ - apk add --update bash && \ - apk add --update wget - -ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.9.0/wait /wait -RUN chmod +x /wait - -WORKDIR /usr/src/app - -COPY package*.json ./ -RUN npm install - -COPY . . diff --git a/adonis-typings/container.ts b/adonis-typings/container.ts deleted file mode 100644 index 87323c1..0000000 --- a/adonis-typings/container.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Core/Application' { - import { RedisManagerContract } from '@ioc:Adonis/Addons/Redis' - - export interface ContainerBindings { - 'Adonis/Addons/Redis': RedisManagerContract - } -} diff --git a/adonis-typings/events.ts b/adonis-typings/events.ts deleted file mode 100644 index 9c7ee3d..0000000 --- a/adonis-typings/events.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Core/Event' { - import { Redis } from 'ioredis' - import { RedisClusterConnectionContract, RedisConnectionContract } from '@ioc:Adonis/Addons/Redis' - - interface EventsList { - 'redis:ready': { connection: RedisClusterConnectionContract | RedisConnectionContract } - 'redis:connect': { connection: RedisClusterConnectionContract | RedisConnectionContract } - 'redis:error': { - error: any - connection: RedisClusterConnectionContract | RedisConnectionContract - } - 'redis:end': { connection: RedisClusterConnectionContract | RedisConnectionContract } - - 'node:added': { connection: RedisClusterConnectionContract; node: Redis } - 'node:removed': { connection: RedisClusterConnectionContract; node: Redis } - 'node:error': { error: any; connection: RedisClusterConnectionContract; address: string } - } -} diff --git a/adonis-typings/redis.ts b/adonis-typings/redis.ts deleted file mode 100644 index 35c4eb1..0000000 --- a/adonis-typings/redis.ts +++ /dev/null @@ -1,258 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Addons/Redis' { - import { EventEmitter } from 'events' - import { HealthReportEntry } from '@ioc:Adonis/Core/HealthCheck' - import { Redis as IoRedis, RedisOptions, ClusterOptions, Cluster, NodeRole } from 'ioredis' - - /* - |-------------------------------------------------------------------------- - | Helpers - |-------------------------------------------------------------------------- - */ - /** - * Returns factory for a given connection by inspecting it's config. - */ - type GetConnectionFactoryType = - RedisConnectionsList[T] extends RedisClusterConfig - ? RedisClusterConnectionContract - : RedisConnectionContract - - /* - |-------------------------------------------------------------------------- - | PubSub - |-------------------------------------------------------------------------- - */ - /** - * Pubsub subscriber - */ - export type PubSubChannelHandler = (data: T) => Promise | void - export type PubSubPatternHandler = ( - channel: string, - data: T - ) => Promise | void - - /** - * Redis pub/sub methods - */ - export interface RedisPubSubContract { - publish( - channel: string, - message: string, - callback: (error: Error | null, count: number) => void - ): void - publish(channel: string, message: string): Promise - subscribe(channel: string, handler: PubSubChannelHandler | string): void - psubscribe(pattern: string, handler: PubSubPatternHandler | string): void - unsubscribe(channel: string): void - punsubscribe(pattern: string): void - } - - /** - * Shape of the report node for the redis connection report - */ - export type HealthReportNode = { - connection: string - status: string - used_memory: string | null - error: any - } - - /** - * List of commands on the IORedis. We omit their internal events and pub/sub - * handlers, since we our own. - */ - export type IORedisCommands = Omit< - IoRedis, - | 'Promise' - | 'status' - | 'connect' - | 'disconnect' - | 'duplicate' - | 'subscribe' - | 'unsubscribe' - | 'psubscribe' - | 'punsubscribe' - | 'quit' - | 'publish' - | keyof EventEmitter - > - - /* - |-------------------------------------------------------------------------- - | Redis Connections - |-------------------------------------------------------------------------- - */ - /** - * Standard Redis Connection - */ - export interface RedisConnectionContract - extends IORedisCommands, - RedisPubSubContract, - EventEmitter { - status: string - connectionName: string - subscriberStatus?: string - ioConnection: IoRedis - ioSubscriberConnection?: IoRedis - - connect(callback?: () => void): Promise - disconnect(): Promise - getReport(checkForMemory?: boolean): Promise - quit(): Promise - runCommand(command: string, ...args: any[]): Promise | any - } - - /** - * Redis cluster factory interface - */ - export interface RedisClusterConnectionContract - extends IORedisCommands, - RedisPubSubContract, - EventEmitter { - status: string - connectionName: string - subscriberStatus?: string - ioConnection: Cluster - ioSubscriberConnection?: Cluster - - getReport(checkForMemory?: boolean): Promise - connect(callback?: () => void): Promise - nodes(role?: NodeRole): IoRedis[] - disconnect(): Promise - quit(): Promise - runCommand(command: string, ...args: any[]): Promise | any - } - - type Connection = RedisClusterConnectionContract | RedisConnectionContract - - /** - * Redis manager exposes the API to intertact with a redis server. One can make - * use of multiple redis connections by defining them inside `config/redis` - * file. - * - * ```ts - * Redis.connection() // default connection - * Redis.connection('primary') // named connection - * ``` - */ - export interface RedisBaseManagerContract { - /** - * A boolean to know whether health checks have been enabled on one - * or more redis connections or not. - */ - healthChecksEnabled: boolean - - /** - * Number of active redis connection. - */ - activeConnectionsCount: number - activeConnections: { [key: string]: Connection } - - /** - * Fetch a named connection from the defined config inside config/redis file - */ - connection( - name: ConnectionName - ): GetConnectionFactoryType - - /** - * Untyped connection - */ - connection(name: string): RedisConnectionContract | RedisClusterConnectionContract - - /** - * Returns the default connection client - */ - connection(): RedisConnectionContract | RedisClusterConnectionContract - - /** - * Returns the healthcheck report - */ - report(): Promise - - /** - * Quit a named connection. - */ - quit(name?: ConnectionName): Promise - quit(name?: string): Promise - - /** - * Forcefully disconnect a named connection. - */ - disconnect( - name?: ConnectionName - ): Promise - disconnect(name?: string): Promise - - /** - * Quit all redis connections - */ - quitAll(): Promise - - /** - * Disconnect all redis connections - */ - disconnectAll(): Promise - - /** - * Run a given custom command - */ - runCommand(command: string, ...args: any[]): Promise | any - } - - /* - |-------------------------------------------------------------------------- - | Config - |-------------------------------------------------------------------------- - */ - /** - * Shape of standard Redis connection config - */ - export type RedisConnectionConfig = Omit & { - healthCheck?: boolean - port?: string | number - } - - /** - * Shape of cluster config - */ - export type RedisClusterConfig = { - clusters: { host: string; port: number | string }[] - clusterOptions?: ClusterOptions - healthCheck?: boolean - } - - /** - * A list of typed connections defined in the user land using - * the contracts file - */ - export interface RedisConnectionsList {} - - /** - * Define the config properties on this interface and they will appear - * everywhere. - */ - export interface RedisConfig { - connection: keyof RedisConnectionsList - connections: { [P in keyof RedisConnectionsList]: RedisConnectionsList[P] } - } - - /** - * Redis manager proxies all IO methods on the connection - */ - export interface RedisManagerContract - extends RedisBaseManagerContract, - IORedisCommands, - RedisPubSubContract {} - - const Redis: RedisManagerContract - export default Redis -} diff --git a/bin/japaTypes.ts b/bin/japaTypes.ts deleted file mode 100644 index d42cac6..0000000 --- a/bin/japaTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Assert } from '@japa/assert' - -declare module '@japa/runner' { - interface TestContext { - assert: Assert - } -} diff --git a/bin/test.ts b/bin/test.ts index 43dd92b..71d7fc4 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,7 +1,8 @@ +import 'dotenv/config' +import { processCLIArgs, configure, run } from '@japa/runner' import { assert } from '@japa/assert' -import { specReporter } from '@japa/spec-reporter' -import { runFailedTests } from '@japa/run-failed-tests' -import { processCliArgs, configure, run } from '@japa/runner' +import { fileSystem } from '@japa/file-system' +import { expectTypeOf } from '@japa/expect-type' /* |-------------------------------------------------------------------------- @@ -16,15 +17,11 @@ import { processCliArgs, configure, run } from '@japa/runner' | | Please consult japa.dev/runner-config for the config docs. */ +processCLIArgs(process.argv.slice(2)) configure({ - ...processCliArgs(process.argv.slice(2)), - ...{ - files: ['test/**/*.spec.ts'], - plugins: [assert(), runFailedTests()], - reporters: [specReporter()], - importer: (filePath: string) => import(filePath), - forceExit: true, - }, + files: ['test/**/*.spec.ts'], + plugins: [assert(), fileSystem(), expectTypeOf()], + forceExit: true, }) /* diff --git a/config.ts b/config.ts deleted file mode 100644 index 4719347..0000000 --- a/config.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { RedisConnectionConfig, RedisClusterConfig } from '@ioc:Adonis/Addons/Redis' - -/** - * Expected shape of the config accepted by the "redisConfig" - * method - */ -type RedisConfig = { - connections: { - [name: string]: RedisConnectionConfig | RedisClusterConfig - } -} - -/** - * Define config for redis - */ -export function redisConfig( - config: T -): T { - return config -} - -/** - * Pull connections from the config defined inside the "config/redis.ts" - * file - */ -export type InferConnectionsFromConfig = { - [K in keyof T['connections']]: T['connections'][K] -} diff --git a/docker-compose.yml b/docker-compose.yml index 830faa8..963804c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,14 @@ version: '3.4' services: - tests: - image: adonis-redis - network_mode: host - build: - context: . - environment: - REDIS_HOST: 0.0.0.0 - REDIS_PORT: 7007 - REDIS_CLUSTER_PORTS: '7000,7001,7002' - WAIT_HOSTS: 0.0.0.0:7000, 0.0.0.0:7001, 0.0.0.0:7002, 0.0.0.0:7003, 0.0.0.0:7004, 0.0.0.0:7005, 0.0.0.0:7006, 0.0.0.0:7007 - depends_on: - - redis - command: sh -c "/wait && npm run test:docker" redis: - network_mode: host image: grokzen/redis-cluster + ports: + - "7000:7000" + - "7001:7001" + - "7002:7002" + - "7003:7003" + - "7004:7004" + - "7005:7005" environment: REDIS_CLUSTER_IP: 0.0.0.0 IP: 0.0.0.0 diff --git a/example/index.ts b/example/index.ts deleted file mode 100644 index 491e526..0000000 --- a/example/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import Redis from '@ioc:Adonis/Addons/Redis' -Redis.connection().get('foo') diff --git a/factories/main.ts b/factories/main.ts new file mode 100644 index 0000000..5b3fc70 --- /dev/null +++ b/factories/main.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { RedisManagerFactory } from './redis_manager.js' diff --git a/factories/redis_manager.ts b/factories/redis_manager.ts new file mode 100644 index 0000000..cdd436b --- /dev/null +++ b/factories/redis_manager.ts @@ -0,0 +1,21 @@ +import { EmitterFactory } from '@adonisjs/core/factories/events' +import { Application } from '@adonisjs/core/app' +import RedisManager from '../src/redis_manager.js' +import { RedisClusterConfig, RedisConnectionConfig } from '../src/types/main.js' + +export class RedisManagerFactory< + ConnectionsList extends Record, +> { + #config: { + connection: keyof ConnectionsList + connections: ConnectionsList + } + + constructor(config: { connection: keyof ConnectionsList; connections: ConnectionsList }) { + this.#config = config + } + + create(app: Application) { + return new RedisManager(app, this.#config, new EmitterFactory().create(app)) + } +} diff --git a/adonis-typings/index.ts b/index.ts similarity index 50% rename from adonis-typings/index.ts rename to index.ts index d25bcb0..3b91c2c 100644 --- a/adonis-typings/index.ts +++ b/index.ts @@ -1,12 +1,10 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// -/// -/// +import './src/types/extended.js' diff --git a/instructions.md b/instructions.md deleted file mode 100644 index c458ccc..0000000 --- a/instructions.md +++ /dev/null @@ -1,10 +0,0 @@ -The package has been configured successfully. The redis configuration stored inside `config/redis.ts` file relies on the following environment variables and hence we recommend validating them. - -Open the `env.ts` file and paste the following code inside the `Env.rules` object. - -```ts -REDIS_CONNECTION: Env.schema.enum(['local'] as const), -REDIS_HOST: Env.schema.string({ format: 'host' }), -REDIS_PORT: Env.schema.number(), -REDIS_PASSWORD: Env.schema.string.optional(), -``` diff --git a/package.json b/package.json index 8eecdbe..a7d2e35 100644 --- a/package.json +++ b/package.json @@ -1,187 +1,122 @@ { - "name": "@adonisjs/redis", - "version": "7.3.2", - "description": "AdonisJS addon for Redis", - "main": "build/providers/RedisProvider.js", - "exports": { - ".": { - "types": "./build/adonis-typings/index.d.ts", - "require": "./build/providers/RedisProvider.js" - }, - "./config": "./build/config.js", - "./build/config": "./build/config.js", - "./*": "./*" - }, - "files": [ - "build/adonis-typings", - "build/providers", - "build/src", - "build/config.js", - "build/config.d.ts", - "build/templates", - "build/instructions.md" - ], - "types": "build/adonis-typings/index.d.ts", - "scripts": { - "mrm": "mrm --preset=@adonisjs/mrm-preset", - "pretest": "npm run lint", - "test": "docker-compose build && docker-compose run --rm tests", - "clean": "del-cli build", - "test:docker": "FORCE_COLOR=true node -r @adonisjs/require-ts/build/register ./bin/test.ts", - "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", - "compile": "npm run lint && npm run clean && tsc", - "build": "npm run compile && npm run copyfiles", - "commit": "git-cz", - "release": "np --message=\"chore(release): %s\"", - "version": "npm run build", - "prepublishOnly": "npm run build", - "lint": "eslint . --ext=.ts", - "format": "prettier --write .", - "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json adonisjs/redis" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/adonisjs/redis.git" - }, - "keywords": [ - "redis", - "ioredis" - ], - "author": "virk,adonisjs", - "license": "MIT", - "bugs": { - "url": "https://github.com/adonisjs/redis/issues" - }, - "homepage": "https://github.com/adonisjs/redis#readme", - "publishConfig": { - "access": "public", - "tag": "latest" - }, - "devDependencies": { - "@adonisjs/core": "^5.8.6", - "@adonisjs/mrm-preset": "^5.0.3", - "@adonisjs/repl": "^3.1.11", - "@adonisjs/require-ts": "^2.0.12", - "@japa/assert": "^1.3.5", - "@japa/run-failed-tests": "^1.0.8", - "@japa/runner": "^2.1.1", - "@japa/spec-reporter": "^1.2.0", - "@poppinss/dev-utils": "^2.0.3", - "@types/node": "^18.7.15", - "commitizen": "^4.2.5", - "copyfiles": "^2.4.1", - "cz-conventional-changelog": "^3.3.0", - "del-cli": "^5.0.0", - "eslint": "^8.23.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-adonis": "^2.1.0", - "eslint-plugin-prettier": "^4.2.1", - "github-label-sync": "^2.2.0", - "husky": "^8.0.1", - "mrm": "^4.1.0", - "np": "^7.6.2", - "prettier": "^2.7.1", - "typescript": "^4.8.2" - }, - "peerDependencies": { - "@adonisjs/core": "^5.1.0" - }, - "dependencies": { - "@poppinss/utils": "^5.0.0", - "@types/ioredis": "^4.28.10", - "ioredis": "^5.2.3" - }, - "nyc": { - "exclude": [ - "test" - ], - "extension": [ - ".ts" - ] - }, - "husky": { - "hooks": { - "commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js" - } - }, - "config": { - "commitizen": { - "path": "cz-conventional-changelog" - } - }, - "np": { - "contents": ".", - "anyBranch": false, - "yolo": true - }, - "adonisjs": { - "instructionsMd": "./build/instructions.md", - "templates": { - "config": [ - { - "src": "config.txt", - "dest": "redis" - } - ], - "contracts": [ - { - "src": "contract.txt", - "dest": "redis" - } - ] - }, - "types": "@adonisjs/redis", - "providers": [ - "@adonisjs/redis" - ], - "env": { - "REDIS_CONNECTION": "local", - "REDIS_HOST": "127.0.0.1", - "REDIS_PORT": "6379", - "REDIS_PASSWORD": "" - } - }, - "mrmConfig": { - "core": true, - "license": "MIT", - "services": [ - "github-actions" - ], - "minNodeVersion": "14.15.4", - "probotApps": [ - "stale", - "lock" - ], - "runGhActionsOnWindows": false - }, - "eslintConfig": { - "extends": [ - "plugin:adonis/typescriptPackage", - "prettier" - ], - "plugins": [ - "prettier" - ], - "rules": { - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ] - } - }, - "eslintIgnore": [ - "build" - ], - "prettier": { - "trailingComma": "es5", - "semi": false, - "singleQuote": true, - "useTabs": false, - "quoteProps": "consistent", - "bracketSpacing": true, - "arrowParens": "always", - "printWidth": 100 - } + "name": "@adonisjs/redis", + "description": "AdonisJS addon for Redis", + "version": "7.3.2", + "main": "build/index.js", + "type": "module", + "files": [ + "index.ts", + "configure.ts", + "src", + "services", + "providers", + "factories", + "build/configure.js", + "build/configure.d.ts", + "build/configure.d.ts.map", + "build/index.js", + "build/index.d.ts", + "build/index.d.ts.map", + "build/src", + "build/services", + "build/providers", + "build/factories", + "build/stubs", + "build/index.d.ts", + "build/index.d.ts.map", + "build/index.js" + ], + "exports": { + ".": "./build/index.js", + "./services/main": "./build/services/main.js", + "./factories": "./build/factories/main.js", + "./types": "./build/src/types/main.js" + }, + "scripts": { + "pretest": "npm run lint", + "test": "docker-compose build && docker-compose run --rm tests", + "quick:test": "node --experimental-import-meta-resolve --enable-source-maps --loader=ts-node/esm ./bin/test.ts", + "clean": "del-cli build", + "test:docker": "FORCE_COLOR=true node --loader=ts-node/esm ./bin/test.ts", + "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", + "compile": "npm run lint && npm run clean && tsc", + "build": "npm run compile && npm run copyfiles", + "release": "np --message=\"chore(release): %s\"", + "version": "npm run build", + "prepublishOnly": "npm run build", + "lint": "eslint . --ext=.ts", + "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/logger" + }, + "devDependencies": { + "@adonisjs/core": "^6.1.5-8", + "@adonisjs/eslint-config": "^1.1.2", + "@adonisjs/prettier-config": "^1.1.2", + "@adonisjs/tsconfig": "^1.1.2", + "@japa/assert": "2.0.0-1", + "@japa/expect-type": "2.0.0-0", + "@japa/file-system": "2.0.0-1", + "@japa/runner": "3.0.0-6", + "@poppinss/dev-utils": "^2.0.3", + "@swc/core": "^1.3.64", + "@types/node": "^20.3.1", + "copyfiles": "^2.4.1", + "del-cli": "^5.0.0", + "dotenv": "^16.3.0", + "eslint": "^8.43.0", + "github-label-sync": "^2.3.1", + "husky": "^8.0.1", + "np": "^7.6.2", + "prettier": "^2.8.8", + "ts-node": "^10.9.1", + "typescript": "^5.1.3" + }, + "dependencies": { + "@poppinss/utils": "6.5.0-3", + "ioredis": "^5.3.2" + }, + "peerDependencies": { + "@adonisjs/core": "^6.1.5-8" + }, + "author": "virk,adonisjs", + "license": "MIT", + "homepage": "https://github.com/adonisjs/redis#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/adonisjs/redis.git" + }, + "bugs": { + "url": "https://github.com/adonisjs/redis/issues" + }, + "keywords": [ + "redis", + "ioredis" + ], + "eslintConfig": { + "extends": "@adonisjs/eslint-config/typescript-package" + }, + "prettier": "@adonisjs/prettier-config", + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "publishConfig": { + "access": "public", + "tag": "next" + }, + "np": { + "message": "chore(release): %s", + "tag": "next", + "branch": "main", + "anyBranch": false + }, + "c8": { + "reporter": [ + "text", + "html" + ], + "exclude": [ + "tests/**" + ] + } } diff --git a/providers/RedisProvider.ts b/providers/RedisProvider.ts deleted file mode 100644 index 7d653c4..0000000 --- a/providers/RedisProvider.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { ApplicationContract } from '@ioc:Adonis/Core/Application' - -/** - * Provider to bind redis to the container - */ -export default class RedisProvider { - constructor(protected app: ApplicationContract) {} - public static needsApplication = true - - /** - * Register redis health check - */ - protected registerHealthCheck() { - /** - * Do not register healthcheck when not running in web - * or test mode - */ - if (!['web', 'test'].includes(this.app.environment)) { - return - } - - this.app.container.withBindings( - ['Adonis/Core/HealthCheck', 'Adonis/Addons/Redis'], - (HealthCheck, Redis) => { - if (Redis.healthChecksEnabled) { - HealthCheck.addChecker('redis', 'Adonis/Addons/Redis') - } - } - ) - } - - /** - * Define repl bindings - */ - protected defineReplBindings() { - /** - * Do not register repl bindings when not running in "repl" - * environment - */ - if (this.app.environment !== 'repl') { - return - } - - this.app.container.withBindings(['Adonis/Addons/Repl'], (Repl) => { - const { defineReplBindings } = require('../src/Bindings/Repl') - defineReplBindings(this.app, Repl) - }) - } - - /** - * Register the redis binding - */ - public register() { - this.app.container.singleton('Adonis/Addons/Redis', () => { - const config = this.app.container.resolveBinding('Adonis/Core/Config').get('redis', {}) - const emitter = this.app.container.resolveBinding('Adonis/Core/Event') - const { RedisManager } = require('../src/RedisManager') - - return new RedisManager(this.app, config, emitter) - }) - } - - /** - * Registering the health check checker with HealthCheck service - */ - public boot() { - this.registerHealthCheck() - this.defineReplBindings() - } - - /** - * Gracefully shutdown connections when app goes down - */ - public async shutdown() { - const Redis = this.app.container.resolveBinding('Adonis/Addons/Redis') - await Redis.quitAll() - } -} diff --git a/providers/redis_provider.ts b/providers/redis_provider.ts new file mode 100644 index 0000000..ec12b9e --- /dev/null +++ b/providers/redis_provider.ts @@ -0,0 +1,58 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { ApplicationService } from '@adonisjs/core/types' +import { defineReplBindings } from '../src/repl_bindings.js' + +/** + * Provider to bind redis to the container + */ +export default class RedisProvider { + constructor(protected app: ApplicationService) {} + + /** + * Define repl bindings + */ + async #defineReplBindings() { + if (this.app.getEnvironment() !== 'repl') { + return + } + + defineReplBindings(this.app, await this.app.container.make('repl')) + } + + /** + * Register the redis binding + */ + register() { + this.app.container.singleton('redis', async () => { + const { default: RedisManager } = await import('../src/redis_manager.js') + + const emitter = await this.app.container.make('emitter') + const config = this.app.config.get('redis', {}) + + return new RedisManager(this.app, config, emitter) + }) + } + + /** + * Registering the health check checker with HealthCheck service + */ + boot() { + this.#defineReplBindings() + } + + /** + * Gracefully shutdown connections when app goes down + */ + async shutdown() { + const redis = await this.app.container.make('redis') + await redis.quitAll() + } +} diff --git a/src/Bindings/Repl.ts b/src/Bindings/Repl.ts deleted file mode 100644 index f1358ed..0000000 --- a/src/Bindings/Repl.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { ReplContract } from '@ioc:Adonis/Addons/Repl' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' - -/** - * Defune repl bindings. The method must be invoked when application environment - * is set to repl. - */ -export function defineReplBindings(application: ApplicationContract, Repl: ReplContract) { - Repl.addMethod( - 'loadRedis', - (repl) => { - repl.server.context.Redis = application.container.use('Adonis/Addons/Redis') - repl.notify( - `Loaded Redis module. You can access it using the "${repl.colors.underline( - 'Redis' - )}" variable` - ) - }, - { - description: 'Load redis provider and save reference to the "Redis" variable', - } - ) -} diff --git a/src/RedisManager/index.ts b/src/RedisManager/index.ts deleted file mode 100644 index 5c65812..0000000 --- a/src/RedisManager/index.ts +++ /dev/null @@ -1,271 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import { EmitterContract } from '@ioc:Adonis/Core/Event' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' -import { Exception, ManagerConfigValidator } from '@poppinss/utils' - -import { - RedisConfig, - HealthReportNode, - RedisBaseManagerContract, - RedisConnectionContract, - RedisClusterConnectionContract, -} from '@ioc:Adonis/Addons/Redis' - -import { ioMethods } from '../ioMethods' -import { pubsubMethods } from '../pubsubMethods' -import { RedisConnection } from '../RedisConnection' -import { RedisClusterConnection } from '../RedisClusterConnection' - -/** - * Redis manager exposes the API to interact with a redis server. - */ -export class RedisManager implements RedisBaseManagerContract { - /** - * An array of connections with health checks enabled, which means, we always - * create a connection for them, even when they are not used. - */ - private healthCheckConnections: string[] = [] - - /** - * A copy of live connections. We avoid re-creating a new connection - * everytime and re-use connections. - */ - public activeConnections: { - [key: string]: RedisClusterConnectionContract | RedisConnectionContract - } = {} - - /** - * A boolean to know whether health checks have been enabled on one - * or more redis connections or not. - */ - public get healthChecksEnabled() { - return this.healthCheckConnections.length > 0 - } - - /** - * Returns the length of active connections - */ - public get activeConnectionsCount() { - return Object.keys(this.activeConnections).length - } - - constructor( - private application: ApplicationContract, - private config: RedisConfig, - private emitter: EmitterContract - ) { - this.validateConfig() - this.healthCheckConnections = Object.keys(this.config.connections).filter( - (connection) => this.config.connections[connection].healthCheck - ) - } - - /** - * Validate config at runtime - */ - private validateConfig() { - const validator = new ManagerConfigValidator(this.config, 'redis', 'config/redis') - validator.validateDefault('connection') - validator.validateList('connections', 'connection') - } - - /** - * Returns default connnection name - */ - private getDefaultConnection(): string { - return this.config.connection - } - - /** - * Returns an existing connection using it's name or the - * default connection, - */ - private getExistingConnection(name?: string) { - name = name || this.getDefaultConnection() - return this.activeConnections[name] - } - - /** - * Returns config for a given connection - */ - private getConnectionConfig(name: string) { - return this.config.connections[name] - } - - /** - * Returns redis factory for a given named connection - */ - public connection(name?: string): any { - /** - * Using default connection name when actual name is missing - */ - name = name || this.getDefaultConnection() - - /** - * Return cached connection - */ - if (this.activeConnections[name]) { - return this.activeConnections[name] - } - - const config = this.getConnectionConfig(name) - - /** - * Raise error if config for the given name is missing - */ - if (!config) { - throw new Exception(`Define config for "${name}" connection inside "config/redis" file`) - } - - /** - * Create connection and store inside the connection pools - * object, so that we can re-use it later - */ - const connection = (this.activeConnections[name] = config.clusters - ? (new RedisClusterConnection( - name, - config, - this.application - ) as unknown as RedisClusterConnectionContract) - : (new RedisConnection(name, config, this.application) as unknown as RedisConnectionContract)) - - /** - * Forward events to the application event emitter - */ - connection.on('ready', ($connection) => - this.emitter.emit('redis:ready', { connection: $connection }) - ) - connection.on('connect', ($connection) => - this.emitter.emit('redis:connect', { connection: $connection }) - ) - connection.on('error', (error, $connection) => - this.emitter.emit('redis:error', { error, connection: $connection }) - ) - connection.on('node:added', ($connection, node) => - this.emitter.emit('redis:node:added', { node, connection: $connection }) - ) - connection.on('node:removed', (node, $connection) => - this.emitter.emit('redis:node:removed', { node, connection: $connection }) - ) - connection.on('node:error', (error, address, $connection) => - this.emitter.emit('redis:node:error', { error, address, connection: $connection }) - ) - - /** - * Stop tracking the connection after it's removed - */ - connection.on('end', ($connection) => { - delete this.activeConnections[$connection.connectionName] - this.emitter.emit('redis:end', { connection: $connection }) - }) - - /** - * Return connection - */ - return connection - } - - /** - * Quit a named connection or the default connection when no - * name is defined. - */ - public async quit(name?: string): Promise { - const connection = this.getExistingConnection(name) - if (!connection) { - return - } - - return connection.quit() - } - - /** - * Disconnect a named connection or the default connection when no - * name is defined. - */ - public async disconnect(name?: string): Promise { - const connection = this.getExistingConnection(name) - if (!connection) { - return - } - - return connection.disconnect() - } - - /** - * Quit all connections - */ - public async quitAll(): Promise { - await Promise.all(Object.keys(this.activeConnections).map((name) => this.quit(name))) - } - - /** - * Disconnect all connections - */ - public async disconnectAll(): Promise { - await Promise.all(Object.keys(this.activeConnections).map((name) => this.disconnect(name))) - } - - /** - * Returns the report for all connections marked for `healthChecks` - */ - public async report() { - const reports = (await Promise.all( - this.healthCheckConnections.map((connection) => { - return this.connection(connection).getReport(true) - }) - )) as HealthReportNode[] - - const healthy = !reports.find((report) => !!report.error) - return { - displayName: 'Redis', - health: { - healthy, - message: healthy - ? 'All connections are healthy' - : 'One or more redis connections are not healthy', - }, - meta: reports, - } - } - - /** - * Define a custom command using LUA script. You can run the - * registered command using the "runCommand" method. - */ - public defineCommand(...args: Parameters): this { - this.connection().defineCommand(...args) - return this - } - - /** - * Run a pre registered command - */ - public runCommand(command: string, ...args: any[]): any { - return this.connection().runCommand(command, ...args) - } -} - -/** - * Since types in AdonisJS are derived from interfaces, we take the leverage - * of dynamically adding redis methods to the class prototype. - */ -pubsubMethods.forEach((method) => { - RedisManager.prototype[method] = function redisManagerProxyFn(...args: any[]) { - return this.connection()[method](...args) - } -}) -ioMethods.forEach((method) => { - RedisManager.prototype[method] = function redisManagerProxyFn(...args: any[]) { - return this.connection()[method](...args) - } -}) diff --git a/src/AbstractConnection/index.ts b/src/abstract_connection.ts similarity index 77% rename from src/AbstractConnection/index.ts rename to src/abstract_connection.ts index 928056e..9b7e227 100644 --- a/src/AbstractConnection/index.ts +++ b/src/abstract_connection.ts @@ -1,25 +1,18 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - -import { EventEmitter } from 'events' +import { EventEmitter } from 'node:events' import { Redis, Cluster } from 'ioredis' import { Exception } from '@poppinss/utils' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' -import { ContainerBindings, IocResolverContract } from '@ioc:Adonis/Core/Application' -import { - HealthReportNode, - PubSubChannelHandler, - PubSubPatternHandler, -} from '@ioc:Adonis/Addons/Redis' +import { ApplicationService } from '@adonisjs/core/types' +import { PubSubChannelHandler, PubSubPatternHandler, HealthReportNode } from './types/main.js' /** * Helper to sleep @@ -34,28 +27,23 @@ export abstract class AbstractConnection extends Even /** * Reference to the main ioRedis connection */ - public ioConnection: T + ioConnection!: T /** * Reference to the main ioRedis subscriber connection */ - public ioSubscriberConnection?: T + ioSubscriberConnection?: T /** * Number of times `getReport` was deferred, at max we defer it for 3 times */ - private deferredReportAttempts = 0 + #deferredReportAttempts = 0 /** * The last error emitted by the `error` event. We set it to `null` after * the `ready` event */ - private lastError?: any - - /** - * IoCResolver to resolve bindings - */ - private resolver: IocResolverContract + #lastError?: any /** * A list of active subscription and pattern subscription @@ -63,20 +51,10 @@ export abstract class AbstractConnection extends Even protected subscriptions: Map = new Map() protected psubscriptions: Map = new Map() - /** - * Returns an anonymous function by parsing the IoC container - * binding. - */ - private resolveIoCBinding(handler: string): PubSubChannelHandler | PubSubPatternHandler { - return (...args: any[]) => { - return this.resolver.call(handler, undefined, args) - } - } - /** * Returns the memory usage for a given connection */ - private async getUsedMemory() { + async #getUsedMemory() { const memory = await (this.ioConnection as Redis).info('memory') const memorySegment = memory .split(/\r|\r\n/) @@ -87,7 +65,7 @@ export abstract class AbstractConnection extends Even /** * Returns status of the main connection */ - public get status(): string { + get status(): string { return (this.ioConnection as Redis).status } @@ -96,7 +74,7 @@ export abstract class AbstractConnection extends Even * undefined when there is no subscriber * connection */ - public get subscriberStatus(): string | undefined { + get subscriberStatus(): string | undefined { if (!this.ioSubscriberConnection) { return } @@ -109,9 +87,8 @@ export abstract class AbstractConnection extends Even */ protected abstract makeSubscriberConnection(): void - constructor(public connectionName: string, application: ApplicationContract) { + constructor(public connectionName: string, application: ApplicationService) { super() - this.resolver = application.container.getResolver(undefined, 'redisListeners', 'App/Listeners') } /** @@ -127,12 +104,12 @@ export abstract class AbstractConnection extends Even * We must set the error to null when server is ready for accept * command */ - this.lastError = null + this.#lastError = null this.emit('ready', this) }) this.ioConnection.on('error', (error: any) => { - this.lastError = error + this.#lastError = error this.emit('error', error, this) }) @@ -200,11 +177,11 @@ export abstract class AbstractConnection extends Even this.ioSubscriberConnection!.on('connect', () => this.emit('subscriber:connect', this)) this.ioSubscriberConnection!.on('ready', () => this.emit('subscriber:ready', this)) this.ioSubscriberConnection!.on('error', (error: any) => - this.emit('subscriber:error', error, this) + this.emit('subscriber:error', error, this), ) this.ioSubscriberConnection!.on('close', () => this.emit('subscriber:close', this)) this.ioSubscriberConnection!.on('reconnecting', () => - this.emit('subscriber:reconnecting', this) + this.emit('subscriber:reconnecting', this), ) /** @@ -226,7 +203,7 @@ export abstract class AbstractConnection extends Even /** * Gracefully end the redis connection */ - public async quit() { + async quit() { await this.ioConnection.quit() if (this.ioSubscriberConnection) { await this.ioSubscriberConnection.quit() @@ -236,7 +213,7 @@ export abstract class AbstractConnection extends Even /** * Forcefully end the redis connection */ - public async disconnect() { + async disconnect() { await this.ioConnection.disconnect() if (this.ioSubscriberConnection) { await this.ioSubscriberConnection.disconnect() @@ -247,7 +224,7 @@ export abstract class AbstractConnection extends Even * Subscribe to a given channel to receive Redis pub/sub events. A * new subscriber connection will be created/managed automatically. */ - public subscribe(channel: string, handler: PubSubChannelHandler | string): void { + subscribe(channel: string, handler: PubSubChannelHandler): void { /** * Make the subscriber connection. The method results in a noop when * subscriber connection already exists. @@ -258,11 +235,10 @@ export abstract class AbstractConnection extends Even * Disallow multiple subscriptions to a single channel */ if (this.subscriptions.has(channel)) { - throw new Exception( - `"${channel}" channel already has an active subscription`, - 500, - 'E_MULTIPLE_REDIS_SUBSCRIPTIONS' - ) + throw new Exception(`"${channel}" channel already has an active subscription`, { + code: 'E_MULTIPLE_REDIS_SUBSCRIPTIONS', + status: 500, + }) } /** @@ -274,9 +250,6 @@ export abstract class AbstractConnection extends Even connection .subscribe(channel) .then((count) => { - if (typeof handler === 'string') { - handler = this.resolveIoCBinding(handler) as PubSubChannelHandler - } this.emit('subscription:ready', count, this) this.subscriptions.set(channel, handler) }) @@ -288,7 +261,7 @@ export abstract class AbstractConnection extends Even /** * Unsubscribe from a channel */ - public unsubscribe(channel: string) { + unsubscribe(channel: string) { this.subscriptions.delete(channel) return (this.ioSubscriberConnection as Redis).unsubscribe(channel) } @@ -296,7 +269,7 @@ export abstract class AbstractConnection extends Even /** * Make redis subscription for a pattern */ - public psubscribe(pattern: string, handler: PubSubPatternHandler | string): void { + psubscribe(pattern: string, handler: PubSubPatternHandler): void { /** * Make the subscriber connection. The method results in a noop when * subscriber connection already exists. @@ -307,11 +280,10 @@ export abstract class AbstractConnection extends Even * Disallow multiple subscriptions to a single channel */ if (this.psubscriptions.has(pattern)) { - throw new Exception( - `${pattern} pattern already has an active subscription`, - 500, - 'E_MULTIPLE_REDIS_PSUBSCRIPTIONS' - ) + throw new Exception(`${pattern} pattern already has an active subscription`, { + status: 500, + code: 'E_MULTIPLE_REDIS_PSUBSCRIPTIONS', + }) } /** @@ -324,10 +296,6 @@ export abstract class AbstractConnection extends Even connection .psubscribe(pattern) .then((count) => { - if (typeof handler === 'string') { - handler = this.resolveIoCBinding(handler) as PubSubPatternHandler - } - this.emit('psubscription:ready', count, this) this.psubscriptions.set(pattern, handler) }) @@ -339,7 +307,7 @@ export abstract class AbstractConnection extends Even /** * Unsubscribe from a given pattern */ - public punsubscribe(pattern: string) { + punsubscribe(pattern: string) { this.psubscriptions.delete(pattern) return (this.ioSubscriberConnection as any).punsubscribe(pattern) } @@ -347,7 +315,7 @@ export abstract class AbstractConnection extends Even /** * Returns report for the connection */ - public async getReport(checkForMemory?: boolean): Promise { + async getReport(checkForMemory?: boolean): Promise { const connection = this.ioConnection as Redis /** @@ -355,9 +323,13 @@ export abstract class AbstractConnection extends Even * the report. Which means, if we are unable to connect to redis within * 3 seconds, we consider the connection unstable. */ - if (connection.status === 'connecting' && this.deferredReportAttempts < 3 && !this.lastError) { + if ( + connection.status === 'connecting' && + this.#deferredReportAttempts < 3 && + !this.#lastError + ) { await sleep() - this.deferredReportAttempts++ + this.#deferredReportAttempts++ return this.getReport(checkForMemory) } @@ -370,7 +342,7 @@ export abstract class AbstractConnection extends Even connection: this.connectionName, status: connection.status, used_memory: null, - error: this.lastError, + error: this.#lastError, } } @@ -383,7 +355,7 @@ export abstract class AbstractConnection extends Even /** * Collect memory when checkForMemory = true */ - const memory = checkForMemory ? await this.getUsedMemory() : 'unknown' + const memory = checkForMemory ? await this.#getUsedMemory() : 'unknown' return { connection: this.connectionName, @@ -404,7 +376,7 @@ export abstract class AbstractConnection extends Even /** * Publish the pub/sub message */ - public publish(channel: string, message: string, callback?: any) { + publish(channel: string, message: string, callback?: any) { return callback ? this.ioConnection.publish(channel, message, callback) : this.ioConnection.publish(channel, message) @@ -414,7 +386,7 @@ export abstract class AbstractConnection extends Even * Define a custom command using LUA script. You can run the * registered command using the "runCommand" method. */ - public defineCommand(...args: Parameters): this { + defineCommand(...args: Parameters): this { this.ioConnection.defineCommand(...args) return this } @@ -422,7 +394,8 @@ export abstract class AbstractConnection extends Even /** * Run a pre registered command */ - public runCommand(command: string, ...args: any[]): any { + runCommand(command: string, ...args: any[]): any { + // @ts-ignore return this.ioConnection[command](...args) } } diff --git a/src/define_config.ts b/src/define_config.ts new file mode 100644 index 0000000..5ccc094 --- /dev/null +++ b/src/define_config.ts @@ -0,0 +1,44 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { RedisConnectionConfig, RedisClusterConfig } from './types/main.js' +import { InvalidArgumentsException } from '@poppinss/utils' + +/** + * Expected shape of the config accepted by the "defineConfig" + * method + */ +type RedisConfig = { + connections: { + [name: string]: RedisConnectionConfig | RedisClusterConfig + } +} + +/** + * Define config for redis + */ +export function defineConfig( + config: T, +): T { + if (!config) { + throw new InvalidArgumentsException('Invalid config. It must be a valid object') + } + + if (!config.connections) { + throw new InvalidArgumentsException('Invalid config. Missing property "connections" inside it') + } + + if (!config.connection || !config.connections[config.connection as any]) { + throw new InvalidArgumentsException( + 'Invalid config. Missing property "connection" or the connection name is not defined inside "connections" object', + ) + } + + return config +} diff --git a/src/ioMethods.ts b/src/io_methods.ts similarity index 98% rename from src/ioMethods.ts rename to src/io_methods.ts index fe4d187..0692efb 100644 --- a/src/ioMethods.ts +++ b/src/io_methods.ts @@ -1,7 +1,7 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -170,4 +170,4 @@ export const ioMethods = [ 'xreadgroup', 'xrevrange', 'xtrim', -] +] as const diff --git a/src/pubsubMethods.ts b/src/pubsub_methods.ts similarity index 88% rename from src/pubsubMethods.ts rename to src/pubsub_methods.ts index 15c516c..bad9f15 100644 --- a/src/pubsubMethods.ts +++ b/src/pubsub_methods.ts @@ -1,7 +1,7 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/RedisClusterConnection/index.ts b/src/redis_cluster_connection.ts similarity index 50% rename from src/RedisClusterConnection/index.ts rename to src/redis_cluster_connection.ts index 24b6870..19aae4d 100644 --- a/src/RedisClusterConnection/index.ts +++ b/src/redis_cluster_connection.ts @@ -1,34 +1,32 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - import Redis, { Cluster, NodeRole } from 'ioredis' -import { RedisClusterConfig } from '@ioc:Adonis/Addons/Redis' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' -import { ioMethods } from '../ioMethods' -import { AbstractConnection } from '../AbstractConnection' +import { ioMethods } from './io_methods.js' +import { AbstractConnection } from './abstract_connection.js' +import { ApplicationService } from '@adonisjs/core/types' +import { RedisClusterConfig, RedisClusterConnectionFactory } from './types/main.js' /** * Redis cluster connection exposes the API to run Redis commands using `ioredis` as the * underlying client. The class abstracts the need of creating and managing multiple * pub/sub connections by hand, since it handles that internally by itself. */ -export class RedisClusterConnection extends AbstractConnection { - constructor( - connectionName: string, - private config: RedisClusterConfig, - application: ApplicationContract - ) { +export class RawRedisClusterConnection extends AbstractConnection { + #config: RedisClusterConfig + + constructor(connectionName: string, config: RedisClusterConfig, application: ApplicationService) { super(connectionName, application) - this.ioConnection = new Redis.Cluster(this.config.clusters as any[], this.config.clusterOptions) + + this.#config = config + this.ioConnection = new Redis.Cluster(this.#config.clusters as any[]) this.proxyConnectionEvents() } @@ -38,21 +36,32 @@ export class RedisClusterConnection extends AbstractConnection { */ protected makeSubscriberConnection() { this.ioSubscriberConnection = new Redis.Cluster( - this.config.clusters as [], - this.config.clusterOptions + this.#config.clusters as [], + this.#config.clusterOptions, ) } /** * Returns cluster nodes */ - public nodes(role?: NodeRole) { + nodes(role?: NodeRole) { return this.ioConnection.nodes(role) } } +/** + * Here we attach pubsub and ioRedis methods to the class. + * + * But we also need to inform typescript about the existence of + * these methods. So we are exporting the class with a + * casted type that has these methods. + */ +const RedisClusterConnection = RawRedisClusterConnection as unknown as RedisClusterConnectionFactory + ioMethods.forEach((method) => { RedisClusterConnection.prototype[method] = function redisConnectionProxyFn(...args: any[]) { return this.ioConnection[method](...args) } }) + +export default RedisClusterConnection diff --git a/src/RedisConnection/index.ts b/src/redis_connection.ts similarity index 53% rename from src/RedisConnection/index.ts rename to src/redis_connection.ts index 479b38d..317609e 100644 --- a/src/RedisConnection/index.ts +++ b/src/redis_connection.ts @@ -1,20 +1,18 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// +import { Redis, RedisOptions } from 'ioredis' -import Redis, { RedisOptions } from 'ioredis' -import { RedisConnectionConfig } from '@ioc:Adonis/Addons/Redis' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' - -import { ioMethods } from '../ioMethods' -import { AbstractConnection } from '../AbstractConnection' +import { ioMethods } from './io_methods.js' +import { AbstractConnection } from './abstract_connection.js' +import { RedisConnectionConfig, RedisConnectionFactory } from './types/main.js' +import { ApplicationService } from '@adonisjs/core/types' /** * Redis connection exposes the API to run Redis commands using `ioredis` as the @@ -22,25 +20,25 @@ import { AbstractConnection } from '../AbstractConnection' * multiple pub/sub connections by hand, since it handles that internally * by itself. */ -export class RedisConnection extends AbstractConnection { - private config: RedisOptions +export class RawRedisConnection extends AbstractConnection { + #config: RedisOptions constructor( connectionName: string, config: RedisConnectionConfig, - application: ApplicationContract + application: ApplicationService, ) { super(connectionName, application) - this.config = this.normalizeConfig(config) + this.#config = this.#normalizeConfig(config) - this.ioConnection = new Redis(this.config) + this.ioConnection = new Redis(this.#config) this.proxyConnectionEvents() } /** * Normalizes config option to be compatible with IORedis */ - private normalizeConfig(config: RedisConnectionConfig): RedisOptions { + #normalizeConfig(config: RedisConnectionConfig): RedisOptions { if (typeof config.port === 'string') { config.port = Number(config.port) } @@ -52,16 +50,23 @@ export class RedisConnection extends AbstractConnection { * invoke this method when first subscription is created. */ protected makeSubscriberConnection() { - this.ioSubscriberConnection = new Redis(this.config) + this.ioSubscriberConnection = new Redis(this.#config) } } /** - * Since types in AdonisJS are derived from interfaces, we take the leverage - * of dynamically adding redis methods to the class prototype. + * Here we attach pubsub and ioRedis methods to the class. + * + * But we also need to inform typescript about the existence of + * these methods. So we are exporting the class with a + * casted type that has these methods. */ +const RedisConnection = RawRedisConnection as unknown as RedisConnectionFactory + ioMethods.forEach((method) => { RedisConnection.prototype[method] = function redisConnectionProxyFn(...args: any[]) { return this.ioConnection[method](...args) } }) + +export default RedisConnection diff --git a/src/redis_manager.ts b/src/redis_manager.ts new file mode 100644 index 0000000..1166c9a --- /dev/null +++ b/src/redis_manager.ts @@ -0,0 +1,280 @@ +import { ApplicationService, EmitterService } from '@adonisjs/core/types' +import RedisConnection from './redis_connection.js' +import { pubsubMethods } from './pubsub_methods.js' +import { ioMethods } from './io_methods.js' +import { + Connection, + GetConnectionType, + RedisConnectionAugmented, + RedisConnectionsList, + RedisManagerFactory, +} from './types/main.js' +import RedisClusterConnection from './redis_cluster_connection.js' + +export class RawRedisManager { + /** + * User provided config + */ + #config: { + connection: keyof ConnectionList + connections: ConnectionList + } + + /** + * Reference to the application + */ + #app: ApplicationService + + /** + * Reference to the emitter + */ + #emitter: EmitterService + + /** + * An array of connections with health checks enabled, which means, we always + * create a connection for them, even when they are not used. + */ + #healthCheckConnections: string[] = [] + + /** + * A copy of live connections. We avoid re-creating a new connection + * everytime and re-use connections. + */ + activeConnections: { + [K in keyof ConnectionList]?: GetConnectionType + } = {} + + /** + * A boolean to know whether health checks have been enabled on one + * or more redis connections or not. + */ + get healthChecksEnabled() { + return this.#healthCheckConnections.length > 0 + } + + /** + * Returns the length of active connections + */ + get activeConnectionsCount() { + return Object.keys(this.activeConnections).length + } + + constructor( + app: ApplicationService, + config: { + connection: keyof ConnectionList + connections: ConnectionList + }, + emitter: EmitterService, + ) { + this.#app = app + this.#config = config + this.#emitter = emitter + this.#healthCheckConnections = Object.keys(this.#config.connections).filter( + (connection) => this.#config.connections[connection].healthCheck, + ) + } + + /** + * Returns the default connection name + */ + #getDefaultConnection(): keyof ConnectionList { + return this.#config.connection + } + + /** + * Returns an existing connection using it's name or the + * default connection, + */ + #getExistingConnection(name?: keyof ConnectionList) { + name = name || this.#getDefaultConnection() + return this.activeConnections[name] + } + + /** + * Returns config for a given connection + */ + #getConnectionConfig(name: ConnectionName) { + return this.#config.connections[name] + } + + /** + * Forward events to the application event emitter + * for a given connection + */ + #forwardConnectionEvents(connection: Connection) { + connection.on('ready', () => { + this.#emitter.emit('redis:ready', { connection }) + }) + connection.on('ready', ($connection) => + this.#emitter.emit('redis:ready', { connection: $connection }), + ) + connection.on('connect', ($connection) => + this.#emitter.emit('redis:connect', { connection: $connection }), + ) + connection.on('error', (error, $connection) => + this.#emitter.emit('redis:error', { error, connection: $connection }), + ) + connection.on('node:added', ($connection, node) => + this.#emitter.emit('redis:node:added', { node, connection: $connection }), + ) + connection.on('node:removed', (node, $connection) => + this.#emitter.emit('redis:node:removed', { node, connection: $connection }), + ) + connection.on('node:error', (error, address, $connection) => + this.#emitter.emit('redis:node:error', { error, address, connection: $connection }), + ) + + /** + * Stop tracking the connection after it's removed + */ + connection.on('end', ($connection) => { + delete this.activeConnections[$connection.connectionName] + this.#emitter.emit('redis:end', { connection: $connection }) + }) + } + + /** + * Returns redis factory for a given named connection + */ + connection( + connectionName?: ConnectionName, + ): GetConnectionType { + const name = connectionName || this.#getDefaultConnection() + + /** + * Return existing connection if already exists + */ + if (this.activeConnections[name]) { + return this.activeConnections[name] as any + } + + /** + * Get config for the named connection + */ + const config = this.#getConnectionConfig(name) + if (!config) { + throw new Error(`Redis connection "${name.toString()}" is not defined`) + } + + /** + * Instantiate the connection based upon the config + */ + const connection = + 'clusters' in config + ? new RedisClusterConnection(name as string, config, this.#app) + : new RedisConnection(name as string, config, this.#app) + + /** + * Cache the connection so that we can re-use it later + */ + this.activeConnections[name] = connection as GetConnectionType + + /** + * Forward ioredis events to the application event emitter + */ + this.#forwardConnectionEvents(connection) + + return connection as GetConnectionType + } + + /** + * Quit a named connection or the default connection when no + * name is defined. + */ + async quit(name?: ConnectionName) { + const connection = this.#getExistingConnection(name) + if (!connection) { + return + } + + return connection.quit() + } + + /** + * Disconnect a named connection or the default connection when no + * name is defined. + */ + async disconnect(name?: ConnectionName) { + const connection = this.#getExistingConnection(name) + if (!connection) { + return + } + + return connection.disconnect() + } + + /** + * Quit all connections + */ + async quitAll(): Promise { + await Promise.all(Object.keys(this.activeConnections).map((name) => this.quit(name))) + } + + /** + * Disconnect all connections + */ + async disconnectAll(): Promise { + await Promise.all(Object.keys(this.activeConnections).map((name) => this.disconnect(name))) + } + + /** + * Returns the report for all connections marked for `healthChecks` + */ + async report() { + const reports = await Promise.all( + this.#healthCheckConnections.map((connection) => this.connection(connection).getReport(true)), + ) + + const healthy = !reports.find((report) => !!report.error) + return { + displayName: 'Redis', + health: { + healthy, + message: healthy + ? 'All connections are healthy' + : 'One or more redis connections are not healthy', + }, + meta: reports, + } + } + + /** + * Define a custom command using LUA script. You can run the + * registered command using the "runCommand" method. + */ + defineCommand(...args: Parameters): this { + this.connection().defineCommand(...args) + return this + } + + /** + * Run a pre registered command + */ + runCommand(command: string, ...args: any[]): any { + return this.connection().runCommand(command, ...args) + } +} + +/** + * Here we attach pubsub and ioRedis methods to the class. + * + * But we also need to inform typescript about the existence of + * these methods. So we are exporting the class with a + * casted type that has these methods. + */ +const RedisManager = RawRedisManager as unknown as RedisManagerFactory + +pubsubMethods.forEach((method) => { + RedisManager.prototype[method] = function redisManagerProxyFn(...args: any[]) { + return this.connection()[method](...args) + } +}) + +ioMethods.forEach((method) => { + RedisManager.prototype[method] = function redisManagerProxyFn(...args: any[]) { + return this.connection()[method](...args) + } +}) + +export default RedisManager diff --git a/src/repl_bindings.ts b/src/repl_bindings.ts new file mode 100644 index 0000000..522ff36 --- /dev/null +++ b/src/repl_bindings.ts @@ -0,0 +1,30 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { Repl } from '@adonisjs/core/repl' +import type { ApplicationService } from '@adonisjs/core/types' + +/** + * Define repl bindings. The method must be invoked when application environment + * is set to repl. + */ +export function defineReplBindings(app: ApplicationService, repl: Repl) { + repl.addMethod( + 'loadRedis', + async () => { + repl.server!.context.redis = await app.container.make('redis') + repl.notify( + `Loaded "redis" service. You can access it using the "${repl.colors.underline( + 'redis', + )}" variable`, + ) + }, + { description: 'Load "redis" service in the REPL context' }, + ) +} diff --git a/src/types/extended.ts b/src/types/extended.ts new file mode 100644 index 0000000..42303be --- /dev/null +++ b/src/types/extended.ts @@ -0,0 +1,35 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { + RedisClusterConnectionAugmented, + RedisConnectionAugmented, + RedisManagerAugmented, +} from './main.js' +import { Redis } from 'ioredis' + +declare module '@adonisjs/core/types' { + export interface ContainerBindings { + redis: RedisManagerAugmented + } + + export interface EventsList { + 'redis:ready': { connection: RedisClusterConnectionAugmented | RedisConnectionAugmented } + 'redis:connect': { connection: RedisClusterConnectionAugmented | RedisConnectionAugmented } + 'redis:error': { + error: any + connection: RedisClusterConnectionAugmented | RedisConnectionAugmented + } + 'redis:end': { connection: RedisClusterConnectionAugmented | RedisConnectionAugmented } + + 'redis:node:added': { connection: RedisClusterConnectionAugmented; node: Redis } + 'redis:node:removed': { connection: RedisClusterConnectionAugmented; node: Redis } + 'redis:node:error': { error: any; connection: RedisClusterConnectionAugmented; address: string } + } +} diff --git a/src/types/main.ts b/src/types/main.ts new file mode 100644 index 0000000..aeb67c8 --- /dev/null +++ b/src/types/main.ts @@ -0,0 +1,151 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { EventEmitter } from 'node:events' +import { Redis as IoRedis, RedisOptions, ClusterOptions, Cluster } from 'ioredis' +import { RawRedisClusterConnection } from '../redis_cluster_connection.js' +import { ApplicationService } from '@adonisjs/core/types' +import { AbstractConnection } from '../abstract_connection.js' +import { RawRedisConnection } from '../redis_connection.js' +import { Emitter } from '@adonisjs/core/events' +import { RawRedisManager } from '../redis_manager.js' + +/* + |-------------------------------------------------------------------------- + | Helpers + |-------------------------------------------------------------------------- + */ +/** + * Returns factory for a given connection by inspecting it's config. + */ +// type GetConnectionFactoryType = +// RedisConnectionsList[T] extends RedisClusterConfig +// ? RedisClusterConnectionContract +// : RedisConnectionContract + +/** + * Pubsub subscriber + */ +export type PubSubChannelHandler = (data: T) => Promise | void +export type PubSubPatternHandler = ( + channel: string, + data: T, +) => Promise | void + +/** + * Redis pub/sub methods + */ +export interface RedisPubSubContract { + publish( + channel: string, + message: string, + callback: (error: Error | null, count: number) => void, + ): void + publish(channel: string, message: string): Promise + subscribe(channel: string, handler: PubSubChannelHandler | string): void + psubscribe(pattern: string, handler: PubSubPatternHandler | string): void + unsubscribe(channel: string): void + punsubscribe(pattern: string): void +} + +/** + * Shape of the report node for the redis connection report + */ +export type HealthReportNode = { + connection: string + status: string + used_memory: string | null + error: any +} + +/** + * List of commands on the IORedis. We omit their internal events and pub/sub + * handlers, since we our own. + */ +export type IORedisCommands = Omit< + IoRedis, + | 'Promise' + | 'status' + | 'connect' + | 'disconnect' + | 'duplicate' + | 'subscribe' + | 'unsubscribe' + | 'psubscribe' + | 'punsubscribe' + | 'quit' + | 'publish' + | keyof EventEmitter +> + +export type Connection = RedisClusterConnectionAugmented | RedisConnectionAugmented +export type RedisConnectionsList = Record + +export type GetConnectionType< + ConnectionsList extends RedisConnectionsList, + T extends keyof ConnectionsList, +> = ConnectionsList[T] extends RedisClusterConfig + ? RedisClusterConnectionAugmented + : RedisConnectionAugmented + +/** + * Shape of standard Redis connection config + */ +export type RedisConnectionConfig = Omit & { + healthCheck?: boolean + port?: string | number +} + +/** + * Shape of cluster config + */ +export type RedisClusterConfig = { + clusters: { host: string; port: number | string }[] + clusterOptions?: ClusterOptions + healthCheck?: boolean +} + +/** + * Since we are dynamically addind methods to the RedisClusterConnection + * RedisConnection and RedisManager classes prototypes, we need to tell + * typescript about it. So the below types represents theses classes with + * all the methods added + */ +export type RedisClusterConnectionFactory = { + new ( + connectionName: string, + config: RedisClusterConfig, + application: ApplicationService, + ): RawRedisClusterConnection & AbstractConnection & IORedisCommands & RedisPubSubContract +} + +export type RedisConnectionFactory = { + new ( + connectionName: string, + config: RedisConnectionConfig, + application: ApplicationService, + ): RawRedisConnection & AbstractConnection & IORedisCommands & RedisPubSubContract +} + +export type RedisManagerFactory = { + new >( + application: ApplicationService, + config: { + connection: keyof ConnectionList + connections: ConnectionList + }, + emitter: Emitter, + ): RawRedisManager & IORedisCommands & RedisPubSubContract +} + +type ConstructorReturnType = T extends { new (...args: any[]): infer U } ? U : never + +export type RedisClusterConnectionAugmented = ConstructorReturnType +export type RedisConnectionAugmented = ConstructorReturnType +export type RedisManagerAugmented = ConstructorReturnType diff --git a/test/contracts.ts b/test/contracts.ts deleted file mode 100644 index e992505..0000000 --- a/test/contracts.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare module '@ioc:Adonis/Addons/Redis' { - export interface RedisConnectionsList { - primary: RedisConnectionConfig - cluster: RedisClusterConfig - } -} diff --git a/test/define_config.spec.ts b/test/define_config.spec.ts new file mode 100644 index 0000000..18d681d --- /dev/null +++ b/test/define_config.spec.ts @@ -0,0 +1,38 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { defineConfig } from '../src/define_config.js' + +test.group('Define Config', () => { + test('should throw if no config passed', ({ assert }) => { + // @ts-ignore + assert.throws(defineConfig, 'Invalid config. It must be a valid object') + }) + + test('should throw if no connections', ({ assert }) => { + assert.throws( + // @ts-ignore + () => defineConfig({ connection: 'hey' }), + 'Invalid config. Missing property "connections" inside it', + ) + }) + + test('should throw if connection is not defined inside connections', ({ assert }) => { + assert.throws( + () => + defineConfig({ + // @ts-ignore + connection: 'hey', + connections: {}, + }), + 'Invalid config. Missing property "connection" or the connection name is not defined inside "connections" object', + ) + }) +}) diff --git a/test/redis-connection.spec.ts b/test/redis-connection.spec.ts deleted file mode 100644 index 1f49991..0000000 --- a/test/redis-connection.spec.ts +++ /dev/null @@ -1,600 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import { test } from '@japa/runner' -import { Application } from '@adonisjs/core/build/standalone' -import { RedisConnectionContract } from '@ioc:Adonis/Addons/Redis' - -import { RedisConnection } from '../src/RedisConnection' - -test.group('Redis factory', () => { - test('emit ready when connected to redis server', ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('error', async (error) => { - console.log(error) - }) - - factory.on('ready', async () => { - assert.isTrue(true) - await factory.quit() - done() - }) - }).waitForDone() - - test('execute redis commands', async ({ assert }) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - await factory.set('greeting', 'hello world') - - const greeting = await factory.get('greeting') - assert.equal(greeting, 'hello world') - - await factory.del('greeting') - await factory.quit() - }) - - test('clean event listeners on quit', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - await factory.quit() - }) - }).waitForDone() - - test('clean event listeners on disconnect', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - await factory.quit() - }) - }).waitForDone() - - test('get event for connection errors', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { port: 4444 }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('error', async (error) => { - assert.equal(error.code, 'ECONNREFUSED') - assert.equal(error.port, 4444) - await factory.quit() - }) - }).waitForDone() - - test('get report for connected connection', async ({ assert }, done) => { - assert.plan(5) - - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - const report = await factory.getReport(true) - - assert.equal(report.status, 'ready') - assert.isNull(report.error) - assert.isDefined(report.used_memory) - - await factory.quit() - }) - }).waitForDone() - - test('get report for errored connection', async ({ assert }, done) => { - assert.plan(5) - - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: 4444, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('error', async () => { - const report = await factory.getReport(true) - - assert.notEqual(report.status, 'ready') - assert.equal(report.error.code, 'ECONNREFUSED') - assert.equal(report.used_memory, null) - - await factory.quit() - }) - }).waitForDone() - - test('execute redis commands using lua scripts', async ({ assert }) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.defineCommand('defineValue', { - numberOfKeys: 1, - lua: `redis.call('set', KEYS[1], ARGV[1])`, - }) - - factory.defineCommand('readValue', { - numberOfKeys: 1, - lua: `return redis.call('get', KEYS[1])`, - }) - - await factory.runCommand('defineValue', 'greeting', 'hello world') - const greeting = await factory.runCommand('readValue', 'greeting') - assert.equal(greeting, 'hello world') - - await factory.del('greeting') - await factory.quit() - }) -}) - -test.group('Redis factory - Subscribe', () => { - test('emit subscriber events when subscriber connection is created', async ({}, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('subscriber:ready', async () => { - await factory.quit() - done() - }) - - factory.subscribe('news', () => {}) - }).waitForDone() - - test('emit subscription event when subscription is created', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('subscription:ready', async (count) => { - assert.equal(count, 1) - await factory.quit() - done() - }) - - factory.subscribe('news', () => {}) - }).waitForDone() - - test('make multiple subscriptions to different channels', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - let invokedCounts = 0 - - factory.on('subscription:ready', async (count) => { - invokedCounts++ - - if (invokedCounts === 2) { - assert.equal(count, 2) - await factory.quit() - done() - } else { - assert.equal(count, 1) - } - }) - - factory.subscribe('news', () => {}) - factory.subscribe('sports', () => {}) - }).waitForDone() - - test('publish messages', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.subscribe('news', async (message) => { - assert.equal(message, 'breaking news at 9') - await factory.quit() - done() - }) - - factory.on('subscription:ready', () => { - factory.publish('news', 'breaking news at 9') - }) - }).waitForDone() - - test('publish messages to multiple channels', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('subscription:ready', (count) => { - if (count === 1) { - factory.publish('news', 'breaking news at 9') - } - }) - - factory.subscribe('news', (message) => { - assert.equal(message, 'breaking news at 9') - factory.publish('sports', 'india won the cup') - }) - - factory.subscribe('sports', async (message) => { - assert.equal(message, 'india won the cup') - await factory.quit() - done() - }) - }).waitForDone() - - test('unsubscribe from a channel', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('subscription:ready', () => { - factory.publish('news', 'breaking news at 9') - }) - - factory.subscribe('news', (message) => { - assert.equal(message, 'breaking news at 9') - factory.unsubscribe('news') - - factory.publish('news', 'breaking news at 9', (_error, count) => { - assert.equal(count, 0) - done() - }) - }) - }).waitForDone() - - test('consume messages not stringified using message builder', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.subscribe('news', async (message) => { - assert.equal(message, 'breaking news at 9') - await factory.quit() - done() - }) - - factory.on('subscription:ready', () => { - factory.ioConnection.publish('news', 'breaking news at 9') - }) - }).waitForDone() - - test('consume messages self stringified with message sub property', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.subscribe('news', async (message) => { - assert.equal(message, JSON.stringify({ message: 'breaking news at 9' })) - await factory.quit() - done() - }) - - factory.on('subscription:ready', () => { - factory.ioConnection.publish('news', JSON.stringify({ message: 'breaking news at 9' })) - }) - }).waitForDone() -}) - -test.group('Redis factory - PSubscribe', () => { - test('emit subscriber events when subscriber connection is created', async ({}, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.on('subscriber:ready', async () => { - await factory.quit() - done() - }) - - factory.psubscribe('news:*', () => {}) - }).waitForDone() - - test('emit subscription event when subscription is created', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.on('psubscription:ready', async (count) => { - assert.equal(count, 1) - await factory.quit() - done() - }) - - factory.psubscribe('news:*', () => {}) - }).waitForDone() - - test('make multiple subscriptions to different patterns', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - let invokedCounts = 0 - - factory.on('psubscription:ready', async (count) => { - invokedCounts++ - - if (invokedCounts === 2) { - assert.equal(count, 2) - await factory.quit() - done() - } else { - assert.equal(count, 1) - } - }) - - factory.psubscribe('news:*', () => {}) - factory.psubscribe('sports:*', () => {}) - }).waitForDone() - - test('publish messages', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.psubscribe('news:*', async (channel, message) => { - assert.equal(channel, 'news:prime') - assert.equal(message, 'breaking news at 9') - await factory.quit() - done() - }) - - factory.on('psubscription:ready', () => { - factory.publish('news:prime', 'breaking news at 9') - }) - }).waitForDone() - - test('publish messages to multiple channels', async ({ assert }, done) => { - assert.plan(2) - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('psubscription:ready', (count) => { - if (count === 1) { - factory.publish('news:prime', 'breaking news at 9') - } - }) - - factory.psubscribe('news:*', async (channel, message) => { - if (channel === 'news:prime') { - assert.equal(message, 'breaking news at 9') - factory.publish('news:breakfast', 'celebrating marathon') - } - - if (channel === 'news:breakfast') { - assert.equal(message, 'celebrating marathon') - await factory.quit() - done() - } - }) - }).waitForDone() - - test('unsubscribe from a pattern', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('psubscription:ready', () => { - factory.publish('news:prime', JSON.stringify({ title: 'breaking news at 9' })) - }) - - factory.psubscribe('news:*', (channel, message) => { - assert.equal(channel, 'news:prime') - assert.deepEqual(message, JSON.stringify({ title: 'breaking news at 9' })) - factory.punsubscribe('news:*') - - factory.publish('news:prime', 'breaking news at 9', (_error, count) => { - assert.equal(count, 0) - done() - }) - }) - }).waitForDone() - - test('bind IoC container binding as subscriber', async ({ assert }, done) => { - const app = new Application(__dirname, 'web', {}) - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - app - ) as unknown as RedisConnectionContract - - class RedisListeners { - public async onNews(channel: string, message: string) { - assert.equal(channel, 'news:prime') - assert.equal(message, JSON.stringify({ title: 'breaking news at 9' })) - await factory.quit() - done() - } - } - - app.container.bind('App/Listeners/RedisListeners', () => { - return new RedisListeners() - }) - - factory.psubscribe('news:*', 'RedisListeners.onNews') - - factory.on('psubscription:ready', () => { - factory.publish('news:prime', JSON.stringify({ title: 'breaking news at 9' })) - }) - }).waitForDone() - - test('consume messages not stringified using message builder', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.psubscribe('news:*', async (channel, message) => { - assert.equal(channel, 'news:prime') - assert.equal(message, 'breaking news at 9') - await factory.quit() - done() - }) - - factory.on('psubscription:ready', () => { - factory.ioConnection.publish('news:prime', 'breaking news at 9') - }) - }).waitForDone() - - test('consume messages self stringified', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.psubscribe('news:*', async (channel, message) => { - assert.equal(channel, 'news:prime') - assert.equal(message, JSON.stringify({ message: 'breaking news at 9' })) - await factory.quit() - done() - }) - - factory.on('psubscription:ready', () => { - factory.ioConnection.publish('news:prime', JSON.stringify({ message: 'breaking news at 9' })) - }) - }).waitForDone() -}) diff --git a/test/redis-provider.spec.ts b/test/redis-provider.spec.ts deleted file mode 100644 index bf661ac..0000000 --- a/test/redis-provider.spec.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * @adonisjs/events - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import { RedisManager } from '../src/RedisManager' - -const fs = new Filesystem(join(__dirname, 'app')) - -async function setup(environment: 'web' | 'repl', redisConfig: any) { - await fs.add('.env', '') - await fs.add( - 'config/app.ts', - ` - export const appKey = 'averylong32charsrandomsecretkey', - export const http = { - cookie: {}, - trustProxy: () => true, - } - ` - ) - - await fs.add( - 'config/redis.ts', - ` - const redisConfig = ${JSON.stringify(redisConfig, null, 2)} - export default redisConfig - ` - ) - - const app = new Application(fs.basePath, environment, { - providers: ['@adonisjs/core', '@adonisjs/repl', '../../providers/RedisProvider'], - }) - - await app.setup() - await app.registerProviders() - await app.bootProviders() - - return app -} - -test.group('Redis Provider', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('register redis provider', async ({ assert }) => { - const app = await setup('web', { - connection: 'local', - connections: { - local: {}, - }, - }) - - assert.instanceOf(app.container.use('Adonis/Addons/Redis'), RedisManager) - assert.deepEqual(app.container.use('Adonis/Addons/Redis')['application'], app) - assert.deepEqual( - app.container.use('Adonis/Addons/Redis'), - app.container.use('Adonis/Addons/Redis') - ) - }) - - test('raise error when config is missing', async ({ assert }) => { - assert.plan(1) - - try { - await setup('web', {}) - } catch (error) { - assert.equal( - error.message, - 'Invalid "redis" config. Missing value for "connection". Make sure to set it inside the "config/redis" file' - ) - } - }) - - test('raise error when primary connection is not defined', async ({ assert }) => { - assert.plan(1) - - try { - await setup('web', {}) - } catch (error) { - assert.equal( - error.message, - 'Invalid "redis" config. Missing value for "connection". Make sure to set it inside the "config/redis" file' - ) - } - }) - - test('raise error when connections are not defined', async ({ assert }) => { - assert.plan(1) - - try { - await setup('web', { - connection: 'local', - }) - } catch (error) { - assert.equal( - error.message, - 'Invalid "redis" config. Missing value for "connections". Make sure to set it inside the "config/redis" file' - ) - } - }) - - test('raise error when primary connection is not defined in the connections list', async ({ - assert, - }) => { - assert.plan(1) - - try { - await setup('web', { - connection: 'local', - connections: {}, - }) - } catch (error) { - assert.equal( - error.message, - 'Invalid "redis" config. "local" is not defined inside "connections". Make sure to set it inside the "config/redis" file' - ) - } - }) - - test('define repl bindings', async ({ assert }) => { - const app = await setup('repl', { - connection: 'local', - connections: { - local: {}, - }, - }) - - assert.property(app.container.use('Adonis/Addons/Repl')['customMethods'], 'loadRedis') - assert.isFunction(app.container.use('Adonis/Addons/Repl')['customMethods']['loadRedis'].handler) - }) - - test('define health checks', async ({ assert }) => { - const app = await setup('web', { - connection: 'local', - connections: { - local: { - healthCheck: true, - }, - }, - }) - - assert.property(app.container.use('Adonis/Core/HealthCheck')['healthCheckers'], 'redis') - assert.equal( - app.container.use('Adonis/Core/HealthCheck')['healthCheckers'].redis, - 'Adonis/Addons/Redis' - ) - }) -}) diff --git a/test/redis.spec.ts b/test/redis.spec.ts deleted file mode 100644 index 9c40ca9..0000000 --- a/test/redis.spec.ts +++ /dev/null @@ -1,311 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import { test } from '@japa/runner' -import { RedisManagerContract } from '@ioc:Adonis/Addons/Redis' -import { Application, Emitter } from '@adonisjs/core/build/standalone' - -import { RedisManager } from '../src/RedisManager' - -const clusterNodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { - return { host: process.env.REDIS_HOST!, port: Number(port) } -}) - -test.group('Redis Manager', () => { - test('run redis commands using default connection', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - await redis.set('greeting', 'hello-world') - const greeting = await redis.get('greeting') - - assert.equal(greeting, 'hello-world') - - await redis.del('greeting') - await redis.quit('primary') - }) - - test('run redis commands using the connection method', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - await redis.connection().set('greeting', 'hello-world') - const greeting = await redis.connection().get('greeting') - assert.equal(greeting, 'hello-world') - - await redis.connection().del('greeting') - await redis.quit('primary') - }) - - test('re-use connection when connection method is called', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - assert.deepEqual(redis.connection(), redis.connection('primary')) - await redis.quit() - }) - - test('connect to redis cluster when cluster array is defined', async ({ assert }, done) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'cluster', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - redis.connection('cluster').on('ready', async () => { - assert.isAbove(redis.connection('cluster').nodes().length, 2) - await redis.quit() - done() - }) - }).waitForDone() - - test('on disconnect clear connection from tracked list', async ({ assert }, done) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - const connection = redis.connection() - connection.on('end', () => { - assert.equal(redis.activeConnectionsCount, 0) - done() - }) - - connection.on('ready', async () => { - await redis.quit() - }) - }).waitForDone() - - test('get report for connections marked for healthChecks', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - healthCheck: true, - }, - secondary: { - host: process.env.REDIS_HOST, - port: 4444, - }, - }, - } as any, - new Emitter(app) - ) - - const report = await redis.report() - assert.deepEqual(report.health, { healthy: true, message: 'All connections are healthy' }) - assert.lengthOf(report.meta, 1) - assert.isDefined(report.meta[0].used_memory) - assert.equal(report.meta[0].status, 'ready') - await redis.quit() - }) - - test('generate correct report when one of the connections are broken', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - healthCheck: true, - }, - secondary: { - host: process.env.REDIS_HOST, - healthCheck: true, - port: 4444, - }, - }, - } as any, - new Emitter(app) - ) - - const report = await redis.report() - - assert.deepEqual(report.health, { - healthy: false, - message: 'One or more redis connections are not healthy', - }) - assert.lengthOf(report.meta, 2) - await redis.quit() - }) - - test('use pub/sub using the manager instance', async ({ assert }, done) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - redis.connection().on('subscription:ready', () => { - redis.publish('news', 'breaking news at 9') - }) - - redis.subscribe('news', async (message) => { - assert.equal(message, 'breaking news at 9') - await redis.quit() - done() - }) - }).waitForDone() - - test('execute redis commands using lua scripts', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - redis.defineCommand('defineValue', { - numberOfKeys: 1, - lua: `redis.call('set', KEYS[1], ARGV[1])`, - }) - - redis.defineCommand('readValue', { - numberOfKeys: 1, - lua: `return redis.call('get', KEYS[1])`, - }) - - await redis.runCommand('defineValue', 'greeting', 'hello world') - const greeting = await redis.runCommand('readValue', 'greeting') - assert.equal(greeting, 'hello world') - - await redis.del('greeting') - await redis.quit() - }) - - test('get and delete the key', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - await redis.set('greeting', 'hello-world') - - assert.equal(await redis.getdel('greeting'), 'hello-world') - assert.isNull(await redis.get('greeting')) - - await redis.quit('primary') - }) -}) diff --git a/test/redis-cluster-factory.spec.ts b/test/redis_cluster_connection.spec.ts similarity index 74% rename from test/redis-cluster-factory.spec.ts rename to test/redis_cluster_connection.spec.ts index 955a804..9c2c6d5 100644 --- a/test/redis-cluster-factory.spec.ts +++ b/test/redis_cluster_connection.spec.ts @@ -1,19 +1,16 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - +import { AppFactory } from '@adonisjs/core/factories/app' import { test } from '@japa/runner' -import { Application } from '@adonisjs/core/build/standalone' -import { RedisClusterConnectionContract } from '@ioc:Adonis/Addons/Redis' - -import { RedisClusterConnection } from '../src/RedisClusterConnection' +import { BASE_URL } from './redis_manager.spec.js' +import RedisClusterConnection from '../src/redis_cluster_connection.js' const nodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { return { host: process.env.REDIS_HOST!, port: Number(port) } @@ -23,11 +20,9 @@ test.group('Redis cluster factory', () => { test('emit ready when connected to redis server', ({ assert }, done) => { const factory = new RedisClusterConnection( 'main', - { - clusters: nodes, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: nodes }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('ready', async () => { assert.isTrue(true) @@ -39,11 +34,9 @@ test.group('Redis cluster factory', () => { test('emit node connection event', ({ assert }, done) => { const factory = new RedisClusterConnection( 'main', - { - clusters: [{ host: process.env.REDIS_HOST!!, port: 7000 }], - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: [{ host: process.env.REDIS_HOST!!, port: 7000 }] }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('node:added', async () => { assert.isTrue(true) @@ -55,11 +48,9 @@ test.group('Redis cluster factory', () => { test('execute redis commands', async ({ assert }) => { const factory = new RedisClusterConnection( 'main', - { - clusters: nodes, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: nodes }, + new AppFactory().create(BASE_URL, () => {}), + ) await factory.set('greeting', 'hello world') const greeting = await factory.get('greeting') @@ -74,11 +65,9 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', - { - clusters: nodes, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: nodes }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -99,8 +88,8 @@ test.group('Redis cluster factory', () => { { clusters: nodes, }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -118,11 +107,9 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', - { - clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }], - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }] }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -145,11 +132,9 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', - { - clusters: nodes, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: nodes }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -168,11 +153,9 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', - { - clusters: nodes, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: nodes }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -196,11 +179,9 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', - { - clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }], - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }] }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -221,11 +202,9 @@ test.group('Redis cluster factory', () => { test('execute redis commands using lua scripts', async ({ assert }) => { const factory = new RedisClusterConnection( 'main', - { - clusters: nodes, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: nodes }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.defineCommand('defineValue', { numberOfKeys: 1, diff --git a/test/redis_connection.spec.ts b/test/redis_connection.spec.ts new file mode 100644 index 0000000..77e6283 --- /dev/null +++ b/test/redis_connection.spec.ts @@ -0,0 +1,190 @@ +import RedisConnection from '../src/redis_connection.js' +import { test } from '@japa/runner' +import { BASE_URL } from './redis_manager.spec.js' +import { AppFactory } from '@adonisjs/core/factories/app' + +test.group('Redis factory', () => { + test('emit ready when connected to redis server', ({ assert }, done) => { + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.on('ready', async () => { + assert.isTrue(true) + await factory.quit() + done() + }) + }).waitForDone() + + test('execute redis commands', async ({ assert }) => { + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + await factory.set('greeting', 'hello world') + + const greeting = await factory.get('greeting') + assert.equal(greeting, 'hello world') + + await factory.del('greeting') + await factory.quit() + }) + + test('clean event listeners on quit', async ({ assert }, done) => { + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.on('end', () => { + assert.equal(factory.ioConnection.listenerCount('ready'), 0) + assert.equal(factory.ioConnection.listenerCount('end'), 0) + done() + }) + + factory.on('ready', async () => { + await factory.quit() + }) + }).waitForDone() + + test('clean event listeners on disconnect', async ({ assert }, done) => { + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.on('end', () => { + assert.equal(factory.ioConnection.listenerCount('ready'), 0) + assert.equal(factory.ioConnection.listenerCount('end'), 0) + done() + }) + + factory.on('ready', async () => { + await factory.quit() + }) + }).waitForDone() + + test('get event for connection errors', async ({ assert }, done) => { + const factory = new RedisConnection( + 'main', + { port: 4444 }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.on('end', () => { + assert.equal(factory.ioConnection.listenerCount('ready'), 0) + assert.equal(factory.ioConnection.listenerCount('end'), 0) + done() + }) + + factory.on('error', async (error) => { + assert.equal(error.code, 'ECONNREFUSED') + assert.equal(error.port, 4444) + await factory.quit() + }) + }).waitForDone() + + test('get report for connected connection', async ({ assert }, done) => { + assert.plan(5) + + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.on('end', () => { + assert.equal(factory.ioConnection.listenerCount('ready'), 0) + assert.equal(factory.ioConnection.listenerCount('end'), 0) + done() + }) + + factory.on('ready', async () => { + const report = await factory.getReport(true) + + assert.equal(report.status, 'ready') + assert.isNull(report.error) + assert.isDefined(report.used_memory) + + await factory.quit() + }) + }).waitForDone() + + test('get report for errored connection', async ({ assert }, done) => { + assert.plan(5) + + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: 4444, + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.on('end', () => { + assert.equal(factory.ioConnection.listenerCount('ready'), 0) + assert.equal(factory.ioConnection.listenerCount('end'), 0) + done() + }) + + factory.on('error', async () => { + const report = await factory.getReport(true) + + assert.notEqual(report.status, 'ready') + assert.equal(report.error.code, 'ECONNREFUSED') + assert.equal(report.used_memory, null) + + await factory.quit() + }) + }).waitForDone() + + test('execute redis commands using lua scripts', async ({ assert }) => { + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.defineCommand('defineValue', { + numberOfKeys: 1, + lua: `redis.call('set', KEYS[1], ARGV[1])`, + }) + + factory.defineCommand('readValue', { + numberOfKeys: 1, + lua: `return redis.call('get', KEYS[1])`, + }) + + await factory.runCommand('defineValue', 'greeting', 'hello world') + const greeting = await factory.runCommand('readValue', 'greeting') + assert.equal(greeting, 'hello world') + + await factory.del('greeting') + await factory.quit() + }) +}) diff --git a/test/redis_manager.spec.ts b/test/redis_manager.spec.ts new file mode 100644 index 0000000..eda4a5d --- /dev/null +++ b/test/redis_manager.spec.ts @@ -0,0 +1,277 @@ +import { test } from '@japa/runner' +import { AppFactory } from '@adonisjs/core/factories/app' +import { RedisManagerFactory } from '../factories/redis_manager.js' +import { RedisClusterConnectionAugmented, RedisConnectionAugmented } from '../src/types/main.js' + +const clusterNodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { + return { host: process.env.REDIS_HOST!, port: Number(port) } +}) + +export const BASE_URL = new URL('./tmp/', import.meta.url) + +test.group('Redis Manager', () => { + test('.connection() types should be inferrred from config', async ({ expectTypeOf }) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + secondary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create(app) + + expectTypeOf(redis.connection).parameter(0).toEqualTypeOf<'primary' | 'secondary' | undefined>() + }) + + test('run redis commands using default connection', async ({ assert }) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create(app) + + await redis.set('greeting', 'hello-world') + const greeting = await redis.get('greeting') + + assert.equal(greeting, 'hello-world') + + await redis.del('greeting') + await redis.quit('primary') + }) + + test('should trigger a ready event when connection is ready', async ({ assert }, done) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create(app) + + redis.connection().on('ready', async () => { + assert.isTrue(true) + await redis.quit() + done() + }) + }) + .waitForDone() + .timeout(5000) + + test('run redis commands using the connection method', async ({ assert }) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create(app) + + await redis.connection().set('greeting', 'hello-world') + const greeting = await redis.connection().get('greeting') + assert.equal(greeting, 'hello-world') + + await redis.connection().del('greeting') + await redis.quit('primary') + }) + + test('re-use connection when connection method is called', async ({ assert }) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create(app) + + assert.deepEqual(redis.connection(), redis.connection()) + await redis.quit() + }) + + test('connect to redis cluster when cluster array is defined', async ({ assert }, done) => { + const app = new AppFactory().create(BASE_URL, () => {}) + + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + cluster: { clusters: clusterNodes }, + }, + }).create(app) + + redis.connection('cluster').on('ready', async () => { + assert.isAbove(redis.connection('cluster').nodes().length, 2) + await redis.quit() + done() + }) + }).waitForDone() + + test('on disconnect clear connection from tracked list', async ({ assert }, done) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create(app) + + const connection = redis.connection() + connection.on('end', () => { + assert.equal(redis.activeConnectionsCount, 0) + done() + }) + + connection.on('ready', async () => { + await redis.quit() + }) + }).waitForDone() + + test('get report for connections marked for healthChecks', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + healthCheck: true, + }, + secondary: { + host: process.env.REDIS_HOST, + port: 4444, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + const report = await redis.report() + assert.deepEqual(report.health, { healthy: true, message: 'All connections are healthy' }) + assert.lengthOf(report.meta, 1) + assert.isDefined(report.meta[0].used_memory) + assert.equal(report.meta[0].status, 'ready') + await redis.quit() + }) + + test('generate correct report when one of the connections are broken', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + healthCheck: true, + }, + secondary: { + host: process.env.REDIS_HOST, + healthCheck: true, + port: 4444, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + const report = await redis.report() + + assert.deepEqual(report.health, { + healthy: false, + message: 'One or more redis connections are not healthy', + }) + assert.lengthOf(report.meta, 2) + await redis.quit() + }) + + test('use pub/sub using the manager instance', async ({ assert }, done) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + cluster: { + clusters: clusterNodes, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + redis.connection().on('subscription:ready', () => { + redis.publish('news', 'breaking news at 9') + }) + + redis.subscribe('news', async (message) => { + assert.equal(message, 'breaking news at 9') + await redis.quit() + done() + }) + }).waitForDone() + + test('execute redis commands using lua scripts', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + cluster: { + clusters: clusterNodes, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + redis.defineCommand('defineValue', { + numberOfKeys: 1, + lua: `redis.call('set', KEYS[1], ARGV[1])`, + }) + + redis.defineCommand('readValue', { + numberOfKeys: 1, + lua: `return redis.call('get', KEYS[1])`, + }) + + await redis.runCommand('defineValue', 'greeting', 'hello world') + const greeting = await redis.runCommand('readValue', 'greeting') + assert.equal(greeting, 'hello world') + + await redis.del('greeting') + await redis.quit() + }) + + test('get and delete the key', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + cluster: { + clusters: clusterNodes, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + await redis.set('greeting', 'hello-world') + + assert.equal(await redis.getdel('greeting'), 'hello-world') + assert.isNull(await redis.get('greeting')) + + await redis.quit('primary') + }) + + test('should have connection types inferred from config', async ({ expectTypeOf }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + cluster: { + clusters: clusterNodes, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + expectTypeOf(redis.connection('cluster')).toEqualTypeOf() + expectTypeOf(redis.connection('primary')).toEqualTypeOf() + }) +}) diff --git a/test/redis_provider.spec.ts b/test/redis_provider.spec.ts new file mode 100644 index 0000000..dd853fa --- /dev/null +++ b/test/redis_provider.spec.ts @@ -0,0 +1,56 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { IgnitorFactory } from '@adonisjs/core/factories' +import { test } from '@japa/runner' + +const BASE_URL = new URL('./tmp/', import.meta.url) + +test.group('Redis Provider', () => { + test('register redis provider', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: ['./providers/redis_provider.js'], + }, + }) + .withCoreConfig() + .create(BASE_URL, { + importer: (filePath) => import(new URL(filePath, new URL('../', import.meta.url)).href), + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + assert.isTrue(app.container.hasBinding('redis')) + }) + + test('define repl bindings', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: ['../providers/redis_provider.js'], + }, + }) + .withCoreProviders() + .create(BASE_URL, { + importer: (filePath) => import(filePath), + }) + + const app = ignitor.createApp('repl') + await app.init() + await app.boot() + + const repl = await app.container.make('repl') + assert.property(repl.getMethods(), 'loadRedis') + assert.isFunction(repl.getMethods().loadRedis.handler) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index db7deb4..ad0cc44 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,7 @@ { - "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", + "extends": "@adonisjs/tsconfig/tsconfig.package.json", "compilerOptions": { - "esModuleInterop": true, - "skipLibCheck": true, - "types": [ - "@adonisjs/core", - "@adonisjs/repl", - "@types/node" - ] + "rootDir": "./", + "outDir": "./build" } }